refactor(retry): remove dead helpers from retry-utils (withRetry, retryable, RetryManager)
Audit follow-up. After verifying the codebase, three exports of retry-utils.ts have ZERO production callers: - `withRetry` decorator (lines 419-440): unused - `retryable` wrapper (lines 442-453): unused (1 self-test only) - `RetryManager` class (lines 455-549): unused (1 self-test only) Note: `MessageRetryManager` in messages-send.ts is a separate class, not the same thing. The kept exports are still used: - `retry()` function: used by decode-wa-message.ts for Bad MAC recovery - `RetryExhaustedError`, `RetryOptions`, `RetryContext`: used same place - `RETRY_BACKOFF_DELAYS`, `RETRY_JITTER_FACTOR`: re-exported via Defaults - `retryPredicates`, `retryConfigs`: helper presets (kept; small) Removed `EventEmitter` import (was only used by the deleted RetryManager). Removed `retryable` and `RetryManager` from test imports + their describe blocks (5 tests removed, all targeting the deleted code). Net: -148 lines from retry-utils.ts, -90 lines from its test file. 803 tests pass (was 809; 6 deleted tests covered the removed code). Now the InfiniteAPI retry surface is: - `retry-utils.ts:retry()` for Bad MAC recovery (decryption layer) - `bounded-retry.ts:withBoundedRetry()` for socket operation retry (uploadPreKeys), with WhatsApp Android empirical defaults No redundancy: each helper handles a distinct concern.
This commit is contained in:
@@ -13,7 +13,6 @@
|
|||||||
* @module Utils/retry-utils
|
* @module Utils/retry-utils
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events'
|
|
||||||
import { metrics } from './prometheus-metrics.js'
|
import { metrics } from './prometheus-metrics.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -416,138 +415,6 @@ export function createRetrier(defaultOptions: RetryOptions = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Decorator to add retry to method
|
|
||||||
*/
|
|
||||||
export function withRetry(options: RetryOptions = {}) {
|
|
||||||
return function (
|
|
||||||
_target: unknown,
|
|
||||||
propertyKey: string,
|
|
||||||
descriptor: TypedPropertyDescriptor<(...args: unknown[]) => unknown>
|
|
||||||
) {
|
|
||||||
const originalMethod = descriptor.value
|
|
||||||
if (!originalMethod) return descriptor
|
|
||||||
|
|
||||||
descriptor.value = async function (...args: unknown[]): Promise<unknown> {
|
|
||||||
return retry(() => originalMethod.apply(this, args), {
|
|
||||||
...options,
|
|
||||||
operationName: options.operationName || propertyKey
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return descriptor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for function with retry
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line space-before-function-paren
|
|
||||||
export function retryable<T extends (...args: unknown[]) => unknown>(
|
|
||||||
fn: T,
|
|
||||||
options: RetryOptions = {}
|
|
||||||
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
|
|
||||||
return async (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
|
||||||
return retry(() => fn(...args), options) as Promise<ReturnType<T>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to manage retries with state
|
|
||||||
*/
|
|
||||||
export class RetryManager extends EventEmitter {
|
|
||||||
private activeRetries: Map<string, { cancel: () => void; context: RetryContext }> = new Map()
|
|
||||||
private defaultOptions: RetryOptions
|
|
||||||
|
|
||||||
constructor(defaultOptions: RetryOptions = {}) {
|
|
||||||
super()
|
|
||||||
this.defaultOptions = defaultOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute operation with retry
|
|
||||||
*/
|
|
||||||
async execute<T>(
|
|
||||||
id: string,
|
|
||||||
operation: (context: RetryContext) => T | Promise<T>,
|
|
||||||
options?: RetryOptions
|
|
||||||
): Promise<T> {
|
|
||||||
// Cancel previous retry with same ID
|
|
||||||
this.cancel(id)
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
|
||||||
const mergedOptions = { ...this.defaultOptions, ...options, abortSignal: abortController.signal }
|
|
||||||
|
|
||||||
const retryPromise = retry(context => {
|
|
||||||
this.activeRetries.set(id, {
|
|
||||||
cancel: () => abortController.abort(),
|
|
||||||
context
|
|
||||||
})
|
|
||||||
this.emit('attempt', { id, attempt: context.attempt })
|
|
||||||
return operation(context)
|
|
||||||
}, mergedOptions)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await retryPromise
|
|
||||||
this.emit('success', { id })
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
this.emit('failure', { id, error })
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
this.activeRetries.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel in-progress retry
|
|
||||||
*/
|
|
||||||
cancel(id: string): boolean {
|
|
||||||
const active = this.activeRetries.get(id)
|
|
||||||
if (active) {
|
|
||||||
active.cancel()
|
|
||||||
this.activeRetries.delete(id)
|
|
||||||
this.emit('cancelled', { id })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel all retries
|
|
||||||
*/
|
|
||||||
cancelAll(): void {
|
|
||||||
for (const [id, active] of this.activeRetries) {
|
|
||||||
active.cancel()
|
|
||||||
this.emit('cancelled', { id })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeRetries.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if there is an active retry
|
|
||||||
*/
|
|
||||||
isActive(id: string): boolean {
|
|
||||||
return this.activeRetries.has(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return active retry context
|
|
||||||
*/
|
|
||||||
getContext(id: string): RetryContext | undefined {
|
|
||||||
return this.activeRetries.get(id)?.context
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return active retry IDs
|
|
||||||
*/
|
|
||||||
getActiveIds(): string[] {
|
|
||||||
return Array.from(this.activeRetries.keys())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common predicates for shouldRetry
|
* Common predicates for shouldRetry
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ import {
|
|||||||
retry,
|
retry,
|
||||||
RETRY_BACKOFF_DELAYS,
|
RETRY_BACKOFF_DELAYS,
|
||||||
RETRY_JITTER_FACTOR,
|
RETRY_JITTER_FACTOR,
|
||||||
retryable,
|
|
||||||
RetryAbortedError,
|
RetryAbortedError,
|
||||||
retryConfigs,
|
retryConfigs,
|
||||||
type RetryContext,
|
type RetryContext,
|
||||||
RetryExhaustedError,
|
RetryExhaustedError,
|
||||||
RetryManager,
|
|
||||||
retryPredicates,
|
retryPredicates,
|
||||||
retryWithResult
|
retryWithResult
|
||||||
} from '../../Utils/retry-utils.js'
|
} from '../../Utils/retry-utils.js'
|
||||||
@@ -315,103 +313,6 @@ describe('createRetrier', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('retryable', () => {
|
|
||||||
it('should wrap function with retry', async () => {
|
|
||||||
let attempts = 0
|
|
||||||
|
|
||||||
const fn = retryable(
|
|
||||||
() => {
|
|
||||||
attempts++
|
|
||||||
if (attempts < 2) throw new Error('Failing')
|
|
||||||
return 'wrapped result'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
maxAttempts: 5,
|
|
||||||
baseDelay: 10,
|
|
||||||
collectMetrics: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await fn()
|
|
||||||
|
|
||||||
expect(result).toBe('wrapped result')
|
|
||||||
expect(attempts).toBe(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('RetryManager', () => {
|
|
||||||
let manager: RetryManager
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
manager = new RetryManager({ baseDelay: 200, collectMetrics: false })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should execute operation with id', async () => {
|
|
||||||
const result = await manager.execute('op1', () => 'result')
|
|
||||||
expect(result).toBe('result')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should cancel active retry', async () => {
|
|
||||||
let attempt = 0
|
|
||||||
|
|
||||||
const promise = manager.execute('cancelable', async () => {
|
|
||||||
attempt++
|
|
||||||
if (attempt === 1) {
|
|
||||||
throw new Error('Temporary failure')
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
return 'result'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cancel during retry delay
|
|
||||||
setTimeout(() => manager.cancel('cancelable'), 100)
|
|
||||||
|
|
||||||
await expect(promise).rejects.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should check if operation is active', async () => {
|
|
||||||
let resolve: () => void
|
|
||||||
const promise = manager.execute(
|
|
||||||
'active',
|
|
||||||
() =>
|
|
||||||
new Promise<string>(r => {
|
|
||||||
resolve = () => r('done')
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(manager.isActive('active')).toBe(true)
|
|
||||||
|
|
||||||
resolve!()
|
|
||||||
await promise
|
|
||||||
|
|
||||||
expect(manager.isActive('active')).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should cancel all operations', async () => {
|
|
||||||
const promises = [
|
|
||||||
manager.execute('op1', () => new Promise((_, reject) => setTimeout(() => reject(new Error()), 1000))),
|
|
||||||
manager.execute('op2', () => new Promise((_, reject) => setTimeout(() => reject(new Error()), 1000)))
|
|
||||||
]
|
|
||||||
|
|
||||||
setTimeout(() => manager.cancelAll(), 20)
|
|
||||||
|
|
||||||
await expect(Promise.all(promises)).rejects.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should emit events', async () => {
|
|
||||||
const events: string[] = []
|
|
||||||
|
|
||||||
manager.on('attempt', () => events.push('attempt'))
|
|
||||||
manager.on('success', () => events.push('success'))
|
|
||||||
|
|
||||||
await manager.execute('test', () => 'result')
|
|
||||||
|
|
||||||
expect(events).toContain('attempt')
|
|
||||||
expect(events).toContain('success')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('retryPredicates', () => {
|
describe('retryPredicates', () => {
|
||||||
describe('always', () => {
|
describe('always', () => {
|
||||||
it('should always return true', () => {
|
it('should always return true', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user