|
1 | 1 | import { describe, expect, it } from 'vitest'; |
2 | | -import { withRetry } from './utils'; |
| 2 | +import { withRetry, withTimeout } from './utils'; |
3 | 3 |
|
4 | | -/** |
5 | | - * Mock function that returns a promise that resolves only when called again after a delay |
6 | | - * This function mocks MetaMask Multichain API wallet_getSession method, where early calls may never resolve |
7 | | - * |
8 | | - * @returns A promise that resolves after a delay |
9 | | - */ |
10 | | -function mockMultichainApiRequest() { |
11 | | - const startTime = Date.now(); |
| 4 | +describe('utils', () => { |
| 5 | + class CustomTimeoutError extends Error {} |
| 6 | + class CustomError extends Error {} |
12 | 7 |
|
13 | | - // Delay for the first call to resolve |
14 | | - const successThresholdDelay = 300; |
| 8 | + describe('withRetry', () => { |
| 9 | + it('retries on thrown error until success', async () => { |
| 10 | + let attempts = 0; |
| 11 | + const fn = async () => { |
| 12 | + attempts++; |
| 13 | + if (attempts < 3) { |
| 14 | + throw new Error('fail'); |
| 15 | + } |
| 16 | + return 'ok'; |
| 17 | + }; |
| 18 | + const result = await withRetry(fn, { maxRetries: 5 }); |
| 19 | + expect(result).toBe('ok'); |
| 20 | + expect(attempts).toBe(3); |
| 21 | + }); |
15 | 22 |
|
16 | | - return async () => { |
17 | | - const callTime = Date.now(); |
18 | | - if (callTime - startTime < successThresholdDelay) { |
19 | | - // Promise that never resolves |
20 | | - await new Promise(() => {}); |
21 | | - } |
22 | | - return 'success'; |
23 | | - }; |
24 | | -} |
| 23 | + it('throws last error after exceeding maxRetries', async () => { |
| 24 | + let attempts = 0; |
| 25 | + const fn = async () => { |
| 26 | + attempts++; |
| 27 | + throw new Error('boom'); |
| 28 | + }; |
| 29 | + await expect(withRetry(fn, { maxRetries: 2 })).rejects.toThrow('boom'); |
| 30 | + // maxRetries=2 => attempts 0,1,2 (3 total) |
| 31 | + expect(attempts).toBe(3); |
| 32 | + }); |
25 | 33 |
|
26 | | -function mockThrowingFn() { |
27 | | - const startTime = Date.now(); |
| 34 | + it('retries only specific error class with delay', async () => { |
| 35 | + let attempts = 0; |
| 36 | + const fn = async () => { |
| 37 | + attempts++; |
| 38 | + if (attempts < 3) { |
| 39 | + throw new CustomError('Custom Error'); |
| 40 | + } |
| 41 | + return 'done'; |
| 42 | + }; |
| 43 | + const start = Date.now(); |
| 44 | + const result = await withRetry(fn, { maxRetries: 5, retryDelay: 20, timeoutErrorClass: CustomTimeoutError }); |
| 45 | + const elapsed = Date.now() - start; |
| 46 | + expect(result).toBe('done'); |
| 47 | + expect(attempts).toBe(3); |
| 48 | + // Two retries with ~20ms delay each (allow some tolerance) |
| 49 | + expect(elapsed).toBeGreaterThanOrEqual(30); |
| 50 | + }); |
28 | 51 |
|
29 | | - // Delay for the first call to resolve |
30 | | - const successThresholdDelay = 300; |
| 52 | + it('retries only TimeoutError class without delay', async () => { |
| 53 | + let attempts = 0; |
| 54 | + const fn = async () => { |
| 55 | + attempts++; |
| 56 | + if (attempts < 3) { |
| 57 | + throw new CustomTimeoutError('Custom Error'); |
| 58 | + } |
| 59 | + return 'done'; |
| 60 | + }; |
| 61 | + const start = Date.now(); |
| 62 | + const result = await withRetry(fn, { maxRetries: 5, retryDelay: 20, timeoutErrorClass: CustomTimeoutError }); |
| 63 | + const elapsed = Date.now() - start; |
| 64 | + expect(result).toBe('done'); |
| 65 | + expect(attempts).toBe(3); |
| 66 | + expect(elapsed).toBeLessThanOrEqual(20); |
| 67 | + }); |
31 | 68 |
|
32 | | - return async () => { |
33 | | - const callTime = Date.now(); |
34 | | - if (callTime - startTime < successThresholdDelay) { |
35 | | - throw new Error('error'); |
36 | | - } |
37 | | - return 'success'; |
38 | | - }; |
39 | | -} |
| 69 | + it('continues retrying even if non-timeout errors occur (no delay applied for them)', async () => { |
| 70 | + const sequenceErrors = [new Error('other'), new CustomTimeoutError('timeout'), new CustomTimeoutError('timeout')]; |
| 71 | + let attempts = 0; |
| 72 | + const fn = async () => { |
| 73 | + if (attempts < sequenceErrors.length) { |
| 74 | + const err = sequenceErrors[attempts]; |
| 75 | + attempts++; |
| 76 | + throw err; |
| 77 | + } |
| 78 | + attempts++; |
| 79 | + return 'final'; |
| 80 | + }; |
| 81 | + const result = await withRetry(fn, { maxRetries: 5, retryDelay: 10, timeoutErrorClass: CustomTimeoutError }); |
| 82 | + expect(result).toBe('final'); |
| 83 | + expect(attempts).toBe(4); // 3 fail + 1 success |
| 84 | + }); |
| 85 | + }); |
40 | 86 |
|
41 | | -describe('utils', () => { |
42 | | - describe('withRetry', () => { |
43 | | - it('should retry a function until it succeeds', async () => { |
44 | | - const result = await withRetry(mockMultichainApiRequest(), { maxRetries: 4, requestTimeout: 100 }); |
45 | | - expect(result).toBe('success'); |
| 87 | + describe('withTimeout', () => { |
| 88 | + it('should resolve before timeout', async () => { |
| 89 | + const result = await withTimeout(Promise.resolve('ok'), 1000); |
| 90 | + expect(result).toBe('ok'); |
46 | 91 | }); |
47 | 92 |
|
48 | | - it('should retry a function that never resolves until it succeeds', async () => { |
49 | | - await expect( |
50 | | - async () => await withRetry(mockMultichainApiRequest(), { maxRetries: 2, requestTimeout: 100 }), |
51 | | - ).rejects.toThrow('Timeout reached'); |
| 93 | + it('should reject after timeout', async () => { |
| 94 | + await expect(withTimeout(new Promise(() => {}), 50)).rejects.toThrow('Timeout after 50ms'); |
52 | 95 | }); |
53 | 96 |
|
54 | | - it('should retry a throwing function until it succeeds', async () => { |
55 | | - const result = await withRetry(mockThrowingFn(), { maxRetries: 4, requestTimeout: 100 }); |
56 | | - expect(result).toBe('success'); |
| 97 | + it('should propagate rejection from promise', async () => { |
| 98 | + await expect(withTimeout(Promise.reject(new Error('fail')), 1000)).rejects.toThrow('fail'); |
57 | 99 | }); |
58 | 100 |
|
59 | | - it('should retry a throwing function until it succeeds', async () => { |
60 | | - await expect( |
61 | | - async () => await withRetry(mockThrowingFn(), { maxRetries: 2, requestTimeout: 100 }), |
62 | | - ).rejects.toThrow('error'); |
| 101 | + it('should use custom error from errorFactory', async () => { |
| 102 | + await expect(withTimeout(new Promise(() => {}), 10, () => new CustomTimeoutError('custom'))).rejects.toThrow( |
| 103 | + CustomTimeoutError, |
| 104 | + ); |
| 105 | + await expect(withTimeout(new Promise(() => {}), 10, () => new CustomTimeoutError('custom'))).rejects.toThrow( |
| 106 | + 'custom', |
| 107 | + ); |
63 | 108 | }); |
64 | 109 | }); |
65 | 110 | }); |
0 commit comments