@wowistudio/grit 1.0.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/LICENSE +21 -0
- package/README.md +304 -0
- package/dist/builder.d.ts +20 -0
- package/dist/builder.js +54 -0
- package/dist/exceptions.d.ts +7 -0
- package/dist/exceptions.js +17 -0
- package/dist/grit.d.ts +18 -0
- package/dist/grit.js +99 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +56 -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,304 @@
|
|
|
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 powerful, fluent retry utility for TypeScript/JavaScript that makes handling transient failures elegant and configurable.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **🔄 Configurable Retries**: Set the number of retry attempts with a simple API
|
|
10
|
+
- **🎯 Error Filtering**: Retry only on specific errors or skip certain error types
|
|
11
|
+
- **⏱️ Flexible Backoff Strategies**: Choose from fixed delays, exponential backoff, or random jitter
|
|
12
|
+
- **🔗 Fluent API**: Chain methods for readable, declarative retry logic
|
|
13
|
+
- **📊 Attempt Tracking**: Access attempt numbers in your retry logic
|
|
14
|
+
- **🎣 Before Retry Hooks**: Execute custom logic before each retry attempt
|
|
15
|
+
- **✅ TypeScript First**: Full type safety with TypeScript support
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Using npm
|
|
21
|
+
npm install @wowistudio/grit
|
|
22
|
+
# Using pnpm
|
|
23
|
+
pnpm add @wowistudio/grit
|
|
24
|
+
# Using yarn
|
|
25
|
+
yarn add @wowistudio/grit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage Examples
|
|
29
|
+
|
|
30
|
+
### Basic
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { Grit } from '@wowistudio/grit';
|
|
34
|
+
|
|
35
|
+
// Simple retry with 3 attempts
|
|
36
|
+
await Grit.retry(3)
|
|
37
|
+
.attempt(async (attempt) => {
|
|
38
|
+
console.log(`Attempt ${attempt}`);
|
|
39
|
+
// Your code that might fail
|
|
40
|
+
await fetchData();
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Error Filtering
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { Grit } from '@wowistudio/grit';
|
|
48
|
+
|
|
49
|
+
// Retry only on specific errors - only retry when NetworkError or TimeoutError occur
|
|
50
|
+
class NetworkError extends Error {}
|
|
51
|
+
class TimeoutError extends Error {}
|
|
52
|
+
|
|
53
|
+
await Grit.retry(5)
|
|
54
|
+
.onlyErrors([NetworkError, TimeoutError])
|
|
55
|
+
.attempt(async () => {
|
|
56
|
+
await makeNetworkRequest();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Skip certain errors - retry on all errors except ValidationError
|
|
60
|
+
class ValidationError extends Error {}
|
|
61
|
+
|
|
62
|
+
await Grit.retry(3)
|
|
63
|
+
.skipErrors([ValidationError])
|
|
64
|
+
.attempt(async () => {
|
|
65
|
+
await processData();
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Delay
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { Grit } from 'grit';
|
|
73
|
+
|
|
74
|
+
// Fixed delay between retries - add a 1 second delay between attempts
|
|
75
|
+
await Grit.retry(3)
|
|
76
|
+
.withDelay(1000)
|
|
77
|
+
.attempt(async () => {
|
|
78
|
+
await apiCall();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Exponential backoff - delays: 100ms, 200ms, 400ms, 800ms, 1600ms
|
|
82
|
+
await Grit.retry(5)
|
|
83
|
+
.withDelay({
|
|
84
|
+
delay: 100, // Initial delay: 100ms
|
|
85
|
+
factor: 2 // Multiply by 2 each time
|
|
86
|
+
})
|
|
87
|
+
.attempt(async () => {
|
|
88
|
+
await apiCall();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Random jitter with exponential backoff - prevents thundering herd problems
|
|
92
|
+
await Grit.retry(4)
|
|
93
|
+
.withDelay({
|
|
94
|
+
minDelay: 100, // Minimum delay: 100ms
|
|
95
|
+
maxDelay: 500, // Maximum delay: 500ms
|
|
96
|
+
factor: 1.5 // Optional: multiply by factor each attempt
|
|
97
|
+
})
|
|
98
|
+
.attempt(async () => {
|
|
99
|
+
await distributedServiceCall();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Custom delay array - specify exact delays for each retry attempt (500ms, 1s, 2s)
|
|
103
|
+
await Grit.retry(3)
|
|
104
|
+
.withDelay([500, 1000, 2000])
|
|
105
|
+
.attempt(async () => {
|
|
106
|
+
await apiCall();
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Reusable Builder Instances
|
|
111
|
+
|
|
112
|
+
You can define a builder instance once and reuse it. Each call to `.attempt()` creates a new execution context, so retry state and attempt counts are independent between calls:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { Grit } from '@wowistudio/grit';
|
|
116
|
+
|
|
117
|
+
// Define a shared builder configuration
|
|
118
|
+
const grit = Grit.retry(3)
|
|
119
|
+
.onlyErrors([NetworkError])
|
|
120
|
+
.beforeRetry((retryCount) => {
|
|
121
|
+
console.log(`Retrying (attempt ${retryCount})...`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Use the same builder for multiple different operations
|
|
125
|
+
const userData = await grit.attempt(() => fetchUserData(userId));
|
|
126
|
+
const orderData = await grit.attempt(() => fetchOrderData(orderId));
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Complete Example
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Grit } from 'grit';
|
|
133
|
+
|
|
134
|
+
// Combining multiple features for a real-world API call
|
|
135
|
+
class RateLimitError extends Error {}
|
|
136
|
+
class NetworkError extends Error {}
|
|
137
|
+
|
|
138
|
+
async function fetchUserData(userId: string) {
|
|
139
|
+
return await Grit.retry(5)
|
|
140
|
+
.onlyErrors([RateLimitError, NetworkError])
|
|
141
|
+
.withDelay(500)
|
|
142
|
+
.beforeRetry(async (retryCount) => {
|
|
143
|
+
console.warn(`Retry ${retryCount} for user ${userId}`);
|
|
144
|
+
})
|
|
145
|
+
.attempt(async (attempt) => {
|
|
146
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
147
|
+
|
|
148
|
+
if (response.status === 429) {
|
|
149
|
+
throw new RateLimitError('Rate limited');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new NetworkError('Network error');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return response.json();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## API Reference
|
|
162
|
+
|
|
163
|
+
### `Grit.retry(retryCount: number)`
|
|
164
|
+
|
|
165
|
+
Creates a new Grit instance with the specified number of retries.
|
|
166
|
+
|
|
167
|
+
**Parameters:**
|
|
168
|
+
- `retryCount` (number): Maximum number of retry attempts
|
|
169
|
+
|
|
170
|
+
**Returns:** A new `Grit` instance
|
|
171
|
+
|
|
172
|
+
### `.attempt(fn: FunctionToExecute)`
|
|
173
|
+
|
|
174
|
+
Executes the provided function with retry logic immediately.
|
|
175
|
+
|
|
176
|
+
**Parameters:**
|
|
177
|
+
- `fn` (Function): An async function that receives the current attempt number as a parameter
|
|
178
|
+
|
|
179
|
+
**Returns:** Promise that resolves with the function's return value or rejects if all retries are exhausted
|
|
180
|
+
|
|
181
|
+
### `.onlyErrors(errors: ErrorConstructor[])`
|
|
182
|
+
|
|
183
|
+
Configures retries to only occur for specific error types.
|
|
184
|
+
|
|
185
|
+
**Parameters:**
|
|
186
|
+
- `errors` (ErrorConstructor[]): Array of error constructors to retry on
|
|
187
|
+
|
|
188
|
+
**Returns:** `Grit` instance for chaining
|
|
189
|
+
|
|
190
|
+
### `.skipErrors(errors: ErrorConstructor[])`
|
|
191
|
+
|
|
192
|
+
Configures retries to skip specific error types (retry on all others).
|
|
193
|
+
|
|
194
|
+
**Parameters:**
|
|
195
|
+
- `errors` (ErrorConstructor[]): Array of error constructors to skip
|
|
196
|
+
|
|
197
|
+
**Returns:** `Grit` instance for chaining
|
|
198
|
+
|
|
199
|
+
### `.withDelay(config: DelayConfig)`
|
|
200
|
+
|
|
201
|
+
Configures the delay strategy between retries.
|
|
202
|
+
|
|
203
|
+
**Parameters:**
|
|
204
|
+
- `config` (DelayConfig): One of:
|
|
205
|
+
- `{ delay: number, factor?: number }`: Fixed delay with optional exponential factor
|
|
206
|
+
- `{ minDelay: number, maxDelay: number, factor?: number }`: Random delay between min/max with optional factor
|
|
207
|
+
- `number[]`: Array of specific delays for each retry (length must match retry count)
|
|
208
|
+
|
|
209
|
+
**Returns:** `Grit` instance for chaining
|
|
210
|
+
|
|
211
|
+
**Throws:** `GritError` if configuration is invalid
|
|
212
|
+
|
|
213
|
+
### `.beforeRetry(fn: (retryCount: number) => void | Promise<void>)`
|
|
214
|
+
|
|
215
|
+
Sets a callback to execute before each retry attempt.
|
|
216
|
+
|
|
217
|
+
**Parameters:**
|
|
218
|
+
- `fn` (Function): Async or sync function that receives the current retry count
|
|
219
|
+
|
|
220
|
+
**Returns:** `Grit` instance for chaining
|
|
221
|
+
|
|
222
|
+
## Delay Configuration
|
|
223
|
+
|
|
224
|
+
### Fixed Delay
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
.withDelay(1000) // 1 second between retries
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Exponential Backoff
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
.withDelay({
|
|
234
|
+
delay: 100, // Start with 100ms
|
|
235
|
+
factor: 2 // Double each time: 100ms, 200ms, 400ms, 800ms...
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Random Jitter
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
.withDelay({
|
|
243
|
+
minDelay: 100, // Minimum 100ms
|
|
244
|
+
maxDelay: 500, // Maximum 500ms
|
|
245
|
+
factor: 1.5 // Optional: scale by 1.5x each attempt
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Custom Delay Array
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
.withDelay([100, 200, 500, 1000]) // Exact delays for each retry
|
|
253
|
+
// Array length must equal retry count
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Error Handling
|
|
257
|
+
|
|
258
|
+
Grit provides two error classes:
|
|
259
|
+
|
|
260
|
+
- **`GritError`**: Thrown for configuration errors
|
|
261
|
+
- **`MaxRetriesError`**: Thrown when maximum retries are exceeded (if configured)
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { Grit, GritError } from 'grit';
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await Grit.retry(3).attempt(async () => {
|
|
268
|
+
// Your code
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof GritError) {
|
|
272
|
+
console.error('Configuration error:', error.message);
|
|
273
|
+
} else {
|
|
274
|
+
console.error('Operation failed after retries:', error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Best Practices
|
|
280
|
+
|
|
281
|
+
1. **Use `onlyErrors` for specific error types**: Don't retry on validation errors or other non-transient failures
|
|
282
|
+
2. **Use exponential backoff for API calls**: Prevents overwhelming services
|
|
283
|
+
3. **Use random jitter for distributed systems**: Prevents thundering herd problems
|
|
284
|
+
4. **Log retry attempts**: Use `beforeRetry` to track retry behavior
|
|
285
|
+
5. **Set reasonable retry counts**: Too many retries can cause long delays
|
|
286
|
+
|
|
287
|
+
## TypeScript Support
|
|
288
|
+
|
|
289
|
+
Grit is written in TypeScript and provides full type safety:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { Grit, DelayConfig, FunctionToExecute } from 'grit';
|
|
293
|
+
|
|
294
|
+
// All types are exported for your use
|
|
295
|
+
const delayConfig: DelayConfig = { delay: 1000 };
|
|
296
|
+
const fn: FunctionToExecute = async (attempt) => {
|
|
297
|
+
// attempt is typed as number
|
|
298
|
+
};
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
This project is licensed under the MIT License.
|
|
304
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GritError } from "./exceptions.ts";
|
|
2
|
+
import type { DelayConfig, FunctionToExecute } from "./types.ts";
|
|
3
|
+
declare class GritBuilder {
|
|
4
|
+
#private;
|
|
5
|
+
private retryCount?;
|
|
6
|
+
_onlyErrors: (typeof GritError)[];
|
|
7
|
+
_skipErrors: (typeof GritError)[];
|
|
8
|
+
fn: FunctionToExecute<any> | undefined;
|
|
9
|
+
$delay: DelayConfig | undefined;
|
|
10
|
+
beforeRetryFn?: (retryCount: number) => void;
|
|
11
|
+
logging: boolean;
|
|
12
|
+
constructor(retryCount: number);
|
|
13
|
+
onlyErrors(errors: (typeof GritError)[]): this;
|
|
14
|
+
skipErrors(errors: (typeof GritError)[]): this;
|
|
15
|
+
withLogging(enabled?: boolean): this;
|
|
16
|
+
withDelay(config: DelayConfig): this;
|
|
17
|
+
beforeRetry(fn: (retryCount: number) => void): this;
|
|
18
|
+
attempt<T>(fn: FunctionToExecute<T>): Promise<T> | T;
|
|
19
|
+
}
|
|
20
|
+
export { GritBuilder };
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
4
|
+
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");
|
|
5
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
6
|
+
};
|
|
7
|
+
var _GritBuilder_instances, _GritBuilder_build;
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.GritBuilder = void 0;
|
|
10
|
+
const grit_ts_1 = require("./grit.ts");
|
|
11
|
+
class GritBuilder {
|
|
12
|
+
constructor(retryCount) {
|
|
13
|
+
_GritBuilder_instances.add(this);
|
|
14
|
+
this._onlyErrors = [];
|
|
15
|
+
this._skipErrors = [];
|
|
16
|
+
this.fn = undefined;
|
|
17
|
+
this.logging = false;
|
|
18
|
+
this.retryCount = retryCount;
|
|
19
|
+
}
|
|
20
|
+
onlyErrors(errors) {
|
|
21
|
+
this._onlyErrors = errors;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
skipErrors(errors) {
|
|
25
|
+
this._skipErrors = errors;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
withLogging(enabled = true) {
|
|
29
|
+
this.logging = enabled;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
withDelay(config) {
|
|
33
|
+
this.$delay = config;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
beforeRetry(fn) {
|
|
37
|
+
this.beforeRetryFn = fn;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
attempt(fn) {
|
|
41
|
+
return __classPrivateFieldGet(this, _GritBuilder_instances, "m", _GritBuilder_build).call(this).attempt(fn);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.GritBuilder = GritBuilder;
|
|
45
|
+
_GritBuilder_instances = new WeakSet(), _GritBuilder_build = function _GritBuilder_build() {
|
|
46
|
+
return new grit_ts_1.Grit({
|
|
47
|
+
retryCount: this.retryCount,
|
|
48
|
+
onlyErrors: this._onlyErrors,
|
|
49
|
+
skipErrors: this._skipErrors,
|
|
50
|
+
delay: this.$delay,
|
|
51
|
+
beforeRetryFn: this.beforeRetryFn,
|
|
52
|
+
logging: this.logging,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MaxRetriesError = exports.GritError = void 0;
|
|
4
|
+
class GritError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "GritError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.GritError = GritError;
|
|
11
|
+
class MaxRetriesError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "MaxRetriesError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.MaxRetriesError = MaxRetriesError;
|
package/dist/grit.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GritBuilder } from "./builder.ts";
|
|
2
|
+
import type { GritProps } from "./types.ts";
|
|
3
|
+
declare class Grit {
|
|
4
|
+
#private;
|
|
5
|
+
private retryCount?;
|
|
6
|
+
private attempts;
|
|
7
|
+
private retries;
|
|
8
|
+
private onlyErrors;
|
|
9
|
+
private skipErrors;
|
|
10
|
+
private delay;
|
|
11
|
+
private beforeRetryFn?;
|
|
12
|
+
private logging;
|
|
13
|
+
constructor(props: GritProps);
|
|
14
|
+
attemptSync<T>(fn: (attempts: number) => T): T;
|
|
15
|
+
attempt<T>(fn: (attempts: number) => Promise<T>): Promise<T>;
|
|
16
|
+
static retry(retryCount: number): GritBuilder;
|
|
17
|
+
}
|
|
18
|
+
export { Grit };
|
package/dist/grit.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
4
|
+
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");
|
|
5
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
6
|
+
};
|
|
7
|
+
var _Grit_instances, _Grit_currentDelay_get, _Grit_handleBeforeRetry, _Grit_backoff, _Grit_backoffSync, _Grit_processError, _Grit_execute, _Grit_executeSync;
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.Grit = void 0;
|
|
10
|
+
const builder_ts_1 = require("./builder.ts");
|
|
11
|
+
const exceptions_ts_1 = require("./exceptions.ts");
|
|
12
|
+
const utils_ts_1 = require("./utils.ts");
|
|
13
|
+
class Grit {
|
|
14
|
+
constructor(props) {
|
|
15
|
+
_Grit_instances.add(this);
|
|
16
|
+
this.attempts = 1;
|
|
17
|
+
this.retries = 0;
|
|
18
|
+
this.logging = false;
|
|
19
|
+
const { retryCount, fn, onlyErrors, skipErrors, delay, beforeRetryFn, logging } = props;
|
|
20
|
+
this.retryCount = retryCount;
|
|
21
|
+
this.onlyErrors = onlyErrors || [];
|
|
22
|
+
this.skipErrors = skipErrors || [];
|
|
23
|
+
this.delay = delay;
|
|
24
|
+
this.beforeRetryFn = beforeRetryFn;
|
|
25
|
+
this.logging = logging || false;
|
|
26
|
+
(0, utils_ts_1.validateGritConfig)(delay, retryCount);
|
|
27
|
+
}
|
|
28
|
+
attemptSync(fn) {
|
|
29
|
+
return __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_executeSync).call(this, fn);
|
|
30
|
+
}
|
|
31
|
+
async attempt(fn) {
|
|
32
|
+
return __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_execute).call(this, fn);
|
|
33
|
+
}
|
|
34
|
+
static retry(retryCount) {
|
|
35
|
+
return new builder_ts_1.GritBuilder(retryCount);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.Grit = Grit;
|
|
39
|
+
_Grit_instances = new WeakSet(), _Grit_currentDelay_get = function _Grit_currentDelay_get() {
|
|
40
|
+
let delay = undefined;
|
|
41
|
+
if (typeof this.delay === "number") {
|
|
42
|
+
delay = this.delay;
|
|
43
|
+
}
|
|
44
|
+
else if (Array.isArray(this.delay)) {
|
|
45
|
+
delay = this.delay.shift();
|
|
46
|
+
}
|
|
47
|
+
else if ((0, utils_ts_1.isObject)(this.delay) && "delay" in this.delay) {
|
|
48
|
+
const { factor, delay: initialDelay } = this.delay;
|
|
49
|
+
delay = initialDelay * Math.pow(factor || 1, this.attempts - 2);
|
|
50
|
+
}
|
|
51
|
+
else if ((0, utils_ts_1.isObject)(this.delay) && "minDelay" in this.delay) {
|
|
52
|
+
const { minDelay, maxDelay, factor } = this.delay;
|
|
53
|
+
const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay;
|
|
54
|
+
delay = factor ? randomDelay * Math.pow(factor, this.attempts - 2) : randomDelay;
|
|
55
|
+
}
|
|
56
|
+
if (!delay)
|
|
57
|
+
throw new exceptions_ts_1.GritError("No delay. Should never happen.");
|
|
58
|
+
if (this.logging)
|
|
59
|
+
console.debug("Delaying for", delay, "ms");
|
|
60
|
+
return delay;
|
|
61
|
+
}, _Grit_handleBeforeRetry = function _Grit_handleBeforeRetry() {
|
|
62
|
+
if (this.retries > 0 && this.beforeRetryFn)
|
|
63
|
+
this.beforeRetryFn(this.retries);
|
|
64
|
+
}, _Grit_backoff = async function _Grit_backoff() {
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, __classPrivateFieldGet(this, _Grit_instances, "a", _Grit_currentDelay_get)));
|
|
66
|
+
}, _Grit_backoffSync = function _Grit_backoffSync() {
|
|
67
|
+
setTimeout(() => { }, __classPrivateFieldGet(this, _Grit_instances, "a", _Grit_currentDelay_get));
|
|
68
|
+
}, _Grit_processError = function _Grit_processError(error) {
|
|
69
|
+
if (error && typeof error === 'object' && 'constructor' in error) {
|
|
70
|
+
if (this.skipErrors.length > 0 && this.skipErrors.includes(error.constructor))
|
|
71
|
+
throw error;
|
|
72
|
+
if (this.onlyErrors.length > 0 && !this.onlyErrors.includes(error.constructor))
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
this.attempts++;
|
|
76
|
+
this.retries++;
|
|
77
|
+
}, _Grit_execute = async function _Grit_execute(fn) {
|
|
78
|
+
try {
|
|
79
|
+
__classPrivateFieldGet(this, _Grit_instances, "m", _Grit_handleBeforeRetry).call(this);
|
|
80
|
+
return await fn(this.attempts);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
__classPrivateFieldGet(this, _Grit_instances, "m", _Grit_processError).call(this, error);
|
|
84
|
+
if (this.delay)
|
|
85
|
+
return __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_backoff).call(this).then(() => __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_execute).call(this, fn));
|
|
86
|
+
return await __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_execute).call(this, fn);
|
|
87
|
+
}
|
|
88
|
+
}, _Grit_executeSync = function _Grit_executeSync(fn) {
|
|
89
|
+
try {
|
|
90
|
+
__classPrivateFieldGet(this, _Grit_instances, "m", _Grit_handleBeforeRetry).call(this);
|
|
91
|
+
return fn(this.attempts);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
__classPrivateFieldGet(this, _Grit_instances, "m", _Grit_processError).call(this, error);
|
|
95
|
+
if (this.delay)
|
|
96
|
+
__classPrivateFieldGet(this, _Grit_instances, "m", _Grit_backoffSync).call(this);
|
|
97
|
+
return __classPrivateFieldGet(this, _Grit_instances, "m", _Grit_executeSync).call(this, fn);
|
|
98
|
+
}
|
|
99
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Grit = exports.GritBuilder = void 0;
|
|
4
|
+
var builder_ts_1 = require("./builder.ts");
|
|
5
|
+
Object.defineProperty(exports, "GritBuilder", { enumerable: true, get: function () { return builder_ts_1.GritBuilder; } });
|
|
6
|
+
var grit_ts_1 = require("./grit.ts");
|
|
7
|
+
Object.defineProperty(exports, "Grit", { enumerable: true, get: function () { return grit_ts_1.Grit; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type GritProps = {
|
|
2
|
+
retryCount?: number;
|
|
3
|
+
fn?: FunctionToExecute<any>;
|
|
4
|
+
onlyErrors?: (typeof Error)[];
|
|
5
|
+
skipErrors?: (typeof Error)[];
|
|
6
|
+
delay?: DelayConfig;
|
|
7
|
+
isConstructor?: boolean;
|
|
8
|
+
beforeRetryFn?: (retryCount: number) => void;
|
|
9
|
+
logging?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type DelayConfig = number | number[] | {
|
|
12
|
+
factor?: number;
|
|
13
|
+
delay: number;
|
|
14
|
+
} | {
|
|
15
|
+
factor?: number;
|
|
16
|
+
minDelay: number;
|
|
17
|
+
maxDelay: number;
|
|
18
|
+
};
|
|
19
|
+
export type FunctionToExecute<T> = (attempts: number) => (T | Promise<T>);
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DelayConfig } from "./types.ts";
|
|
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,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateGritConfig = exports.isPromise = exports.isObject = void 0;
|
|
4
|
+
const exceptions_ts_1 = require("./exceptions.ts");
|
|
5
|
+
const isObject = (o) => {
|
|
6
|
+
return typeof o === 'object' && !Array.isArray(o) && o !== null;
|
|
7
|
+
};
|
|
8
|
+
exports.isObject = isObject;
|
|
9
|
+
const isPromise = (value) => {
|
|
10
|
+
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
11
|
+
};
|
|
12
|
+
exports.isPromise = isPromise;
|
|
13
|
+
const validateGritConfig = (config, retryCount) => {
|
|
14
|
+
if (!config)
|
|
15
|
+
return;
|
|
16
|
+
if (!retryCount)
|
|
17
|
+
throw new exceptions_ts_1.GritError("Missing retry config (Grit.retry(<count>))");
|
|
18
|
+
// validate array config
|
|
19
|
+
if (Array.isArray(config)) {
|
|
20
|
+
if (config.length !== retryCount)
|
|
21
|
+
throw new exceptions_ts_1.GritError("Delay array length must be equal to retry count");
|
|
22
|
+
for (const delay of config) {
|
|
23
|
+
if (delay <= 0)
|
|
24
|
+
throw new exceptions_ts_1.GritError("delay must be greater than 0");
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof config === "number") {
|
|
29
|
+
if (config <= 0)
|
|
30
|
+
throw new exceptions_ts_1.GritError("delay must be greater than 0");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!(0, exports.isObject)(config))
|
|
34
|
+
throw new exceptions_ts_1.GritError("Invalid backoff config");
|
|
35
|
+
// validate delay config
|
|
36
|
+
if ("delay" in config) {
|
|
37
|
+
if (typeof config.delay !== "number")
|
|
38
|
+
throw new exceptions_ts_1.GritError("delay must be a number");
|
|
39
|
+
if (config.delay <= 0)
|
|
40
|
+
throw new exceptions_ts_1.GritError("delay must be greater than 0");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// validate random delay config
|
|
44
|
+
const { minDelay, maxDelay } = config;
|
|
45
|
+
if (!minDelay || typeof minDelay !== "number")
|
|
46
|
+
throw new exceptions_ts_1.GritError("minDelay must be a number");
|
|
47
|
+
if (!maxDelay || typeof maxDelay !== "number")
|
|
48
|
+
throw new exceptions_ts_1.GritError("maxDelay must be a number");
|
|
49
|
+
if (minDelay >= maxDelay)
|
|
50
|
+
throw new exceptions_ts_1.GritError("minDelay must be less than maxDelay");
|
|
51
|
+
if (minDelay <= 0)
|
|
52
|
+
throw new exceptions_ts_1.GritError("minDelay must be greater than 0");
|
|
53
|
+
if (maxDelay <= 0)
|
|
54
|
+
throw new exceptions_ts_1.GritError("maxDelay must be greater than 0");
|
|
55
|
+
};
|
|
56
|
+
exports.validateGritConfig = validateGritConfig;
|
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": "1.0.0",
|
|
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
|
+
}
|