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:
Renato Alcara
2026-04-26 23:35:21 -03:00
parent 5017393277
commit 0f2402da58
2 changed files with 0 additions and 232 deletions
-133
View File
@@ -13,7 +13,6 @@
* @module Utils/retry-utils
*/
import { EventEmitter } from 'events'
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
*/
-99
View File
@@ -11,12 +11,10 @@ import {
retry,
RETRY_BACKOFF_DELAYS,
RETRY_JITTER_FACTOR,
retryable,
RetryAbortedError,
retryConfigs,
type RetryContext,
RetryExhaustedError,
RetryManager,
retryPredicates,
retryWithResult
} 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('always', () => {
it('should always return true', () => {