-
Notifications
You must be signed in to change notification settings - Fork 1
fix: add timeout getMultichainClient to prevent remove connections fr… #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a6b1981
fix: add timeout getMultichainClient to prevent remove connections fr…
elribonazo 66f37bb
Move timeout management to Transport layer
EdouardBougon af5c73b
Fix retry
EdouardBougon f628188
fix lint after rebase
EdouardBougon 5102147
fix: cleanup pendingRequests on timeout to prevent memory leak + tests
EdouardBougon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,65 +1,110 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { withRetry } from './utils'; | ||
| import { withRetry, withTimeout } from './utils'; | ||
|
|
||
| /** | ||
| * Mock function that returns a promise that resolves only when called again after a delay | ||
| * This function mocks MetaMask Multichain API wallet_getSession method, where early calls may never resolve | ||
| * | ||
| * @returns A promise that resolves after a delay | ||
| */ | ||
| function mockMultichainApiRequest() { | ||
| const startTime = Date.now(); | ||
| describe('utils', () => { | ||
| class CustomTimeoutError extends Error {} | ||
| class CustomError extends Error {} | ||
|
|
||
| // Delay for the first call to resolve | ||
| const successThresholdDelay = 300; | ||
| describe('withRetry', () => { | ||
| it('retries on thrown error until success', async () => { | ||
| let attempts = 0; | ||
| const fn = async () => { | ||
| attempts++; | ||
| if (attempts < 3) { | ||
| throw new Error('fail'); | ||
| } | ||
| return 'ok'; | ||
| }; | ||
| const result = await withRetry(fn, { maxRetries: 5 }); | ||
| expect(result).toBe('ok'); | ||
| expect(attempts).toBe(3); | ||
| }); | ||
|
|
||
| return async () => { | ||
| const callTime = Date.now(); | ||
| if (callTime - startTime < successThresholdDelay) { | ||
| // Promise that never resolves | ||
| await new Promise(() => {}); | ||
| } | ||
| return 'success'; | ||
| }; | ||
| } | ||
| it('throws last error after exceeding maxRetries', async () => { | ||
| let attempts = 0; | ||
| const fn = async () => { | ||
| attempts++; | ||
| throw new Error('boom'); | ||
| }; | ||
| await expect(withRetry(fn, { maxRetries: 2 })).rejects.toThrow('boom'); | ||
| // maxRetries=2 => attempts 0,1,2 (3 total) | ||
| expect(attempts).toBe(3); | ||
| }); | ||
|
|
||
| function mockThrowingFn() { | ||
| const startTime = Date.now(); | ||
| it('retries only specific error class with delay', async () => { | ||
| let attempts = 0; | ||
| const fn = async () => { | ||
| attempts++; | ||
| if (attempts < 3) { | ||
| throw new CustomError('Custom Error'); | ||
| } | ||
| return 'done'; | ||
| }; | ||
| const start = Date.now(); | ||
| const result = await withRetry(fn, { maxRetries: 5, retryDelay: 20, timeoutErrorClass: CustomTimeoutError }); | ||
| const elapsed = Date.now() - start; | ||
| expect(result).toBe('done'); | ||
| expect(attempts).toBe(3); | ||
| // Two retries with ~20ms delay each (allow some tolerance) | ||
| expect(elapsed).toBeGreaterThanOrEqual(30); | ||
| }); | ||
|
|
||
| // Delay for the first call to resolve | ||
| const successThresholdDelay = 300; | ||
| it('retries only TimeoutError class without delay', async () => { | ||
| let attempts = 0; | ||
| const fn = async () => { | ||
| attempts++; | ||
| if (attempts < 3) { | ||
| throw new CustomTimeoutError('Custom Error'); | ||
| } | ||
| return 'done'; | ||
| }; | ||
| const start = Date.now(); | ||
| const result = await withRetry(fn, { maxRetries: 5, retryDelay: 20, timeoutErrorClass: CustomTimeoutError }); | ||
| const elapsed = Date.now() - start; | ||
| expect(result).toBe('done'); | ||
| expect(attempts).toBe(3); | ||
| expect(elapsed).toBeLessThanOrEqual(20); | ||
| }); | ||
|
|
||
| return async () => { | ||
| const callTime = Date.now(); | ||
| if (callTime - startTime < successThresholdDelay) { | ||
| throw new Error('error'); | ||
| } | ||
| return 'success'; | ||
| }; | ||
| } | ||
| it('continues retrying even if non-timeout errors occur (no delay applied for them)', async () => { | ||
| const sequenceErrors = [new Error('other'), new CustomTimeoutError('timeout'), new CustomTimeoutError('timeout')]; | ||
| let attempts = 0; | ||
| const fn = async () => { | ||
| if (attempts < sequenceErrors.length) { | ||
| const err = sequenceErrors[attempts]; | ||
| attempts++; | ||
| throw err; | ||
| } | ||
| attempts++; | ||
| return 'final'; | ||
| }; | ||
| const result = await withRetry(fn, { maxRetries: 5, retryDelay: 10, timeoutErrorClass: CustomTimeoutError }); | ||
| expect(result).toBe('final'); | ||
| expect(attempts).toBe(4); // 3 fail + 1 success | ||
| }); | ||
| }); | ||
|
|
||
| describe('utils', () => { | ||
| describe('withRetry', () => { | ||
| it('should retry a function until it succeeds', async () => { | ||
| const result = await withRetry(mockMultichainApiRequest(), { maxRetries: 4, requestTimeout: 100 }); | ||
| expect(result).toBe('success'); | ||
| describe('withTimeout', () => { | ||
| it('should resolve before timeout', async () => { | ||
| const result = await withTimeout(Promise.resolve('ok'), 1000); | ||
| expect(result).toBe('ok'); | ||
| }); | ||
|
|
||
| it('should retry a function that never resolves until it succeeds', async () => { | ||
| await expect( | ||
| async () => await withRetry(mockMultichainApiRequest(), { maxRetries: 2, requestTimeout: 100 }), | ||
| ).rejects.toThrow('Timeout reached'); | ||
| it('should reject after timeout', async () => { | ||
| await expect(withTimeout(new Promise(() => {}), 50)).rejects.toThrow('Timeout after 50ms'); | ||
| }); | ||
|
|
||
| it('should retry a throwing function until it succeeds', async () => { | ||
| const result = await withRetry(mockThrowingFn(), { maxRetries: 4, requestTimeout: 100 }); | ||
| expect(result).toBe('success'); | ||
| it('should propagate rejection from promise', async () => { | ||
| await expect(withTimeout(Promise.reject(new Error('fail')), 1000)).rejects.toThrow('fail'); | ||
| }); | ||
|
|
||
| it('should retry a throwing function until it succeeds', async () => { | ||
| await expect( | ||
| async () => await withRetry(mockThrowingFn(), { maxRetries: 2, requestTimeout: 100 }), | ||
| ).rejects.toThrow('error'); | ||
| it('should use custom error from errorFactory', async () => { | ||
| await expect(withTimeout(new Promise(() => {}), 10, () => new CustomTimeoutError('custom'))).rejects.toThrow( | ||
| CustomTimeoutError, | ||
| ); | ||
| await expect(withTimeout(new Promise(() => {}), 10, () => new CustomTimeoutError('custom'))).rejects.toThrow( | ||
| 'custom', | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.