import assert from 'node:assert/strict' import { test } from 'node:test' import { Client, DiscordRESTError } from 'oceanic.js' import { retryDiscord } from '../src/retry.ts' function resetError(): TypeError { return new TypeError('fetch failed', { cause: Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }) }) } test('Oceanic rejects a connection reset and a retry on the same route succeeds', { timeout: 5000 }, async (t) => { const client = new Client({ rest: { baseURL: 'https://discord.invalid/api/v10' } }) const errors: unknown[] = [] client.on('error', (error) => errors.push(error)) t.mock.method(console, 'warn', () => {}) let requests = 0 const error = resetError() t.mock.method(globalThis, 'fetch', async () => { if (++requests === 1) throw error return new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } }) }) const result = await retryDiscord(() => client.rest.request({ method: 'GET', path: '/test' }), 'test') assert.deepEqual(result, { ok: true }) assert.equal(requests, 2) assert.deepEqual(errors, [error]) }) test('Oceanic rejects timed-out requests rather than leaving them pending', { timeout: 5000 }, async (t) => { const client = new Client({ rest: { baseURL: 'https://discord.invalid/api/v10', requestTimeout: 10 } }) client.on('error', () => {}) t.mock.method(globalThis, 'fetch', (_url: string, options: RequestInit) => new Promise((_resolve, reject) => { options.signal!.addEventListener('abort', () => reject(options.signal!.reason), { once: true }) })) await assert.rejects(client.rest.request({ method: 'GET', path: '/test' }), { name: 'AbortError' }) }) test('transport retries have bounded exponential backoff and preserve the terminal error', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) t.mock.method(console, 'warn', () => {}) const error = resetError() let attempts = 0 const result = assert.rejects(retryDiscord(async () => { attempts++ throw error }, 'test'), (actual) => actual === error) await new Promise((resolve) => setImmediate(resolve)) assert.equal(attempts, 1) for (const wait of [1000, 2000, 4000, 8000]) { const before: number = attempts t.mock.timers.tick(wait - 1) await new Promise((resolve) => setImmediate(resolve)) assert.equal(attempts, before) t.mock.timers.tick(1) await new Promise((resolve) => setImmediate(resolve)) assert.equal(attempts, before + 1) } await result assert.equal(attempts, 5) }) test('permission errors and programming errors are not retried', async () => { const response = new Response('', { status: 403 }) Object.defineProperty(response, 'url', { value: 'https://discord.invalid/api/v10/test' }) for (const error of [new DiscordRESTError(response, { code: 50013, message: 'Missing Permissions' }, 'GET'), new TypeError('bad input')]) { let attempts = 0 await assert.rejects(retryDiscord(async () => { attempts++ throw error }, 'test'), (actual) => actual === error) assert.equal(attempts, 1) } }) test('timeouts and server errors are retried', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) t.mock.method(console, 'warn', () => {}) const response = new Response('', { status: 503 }) Object.defineProperty(response, 'url', { value: 'https://discord.invalid/api/v10/test' }) for (const error of [new DOMException('aborted', 'AbortError'), new DiscordRESTError(response, { code: 0, message: 'Unavailable' }, 'GET')]) { let attempts = 0 const result = retryDiscord(async () => { if (++attempts === 1) throw error return 'recovered' }, 'test') await new Promise((resolve) => setImmediate(resolve)) t.mock.timers.tick(1000) assert.equal(await result, 'recovered') assert.equal(attempts, 2) } })