import Lacuna from '../lacuna'; import Responses, { isBlockingError } from './responses'; import type { ResponseEvent } from './responses'; import type { ServerError, ServerRequest, ServerResponse } from './server'; const request: ServerRequest = { module: 'empire', method: 'get_status', params: {}, addSession: true, }; const eventFor = (response: ServerResponse): ResponseEvent => ({ request, response, attempt: 0, retry: jest.fn() }) as unknown as ResponseEvent; const slowDown: ServerError = { code: 1010, message: 'Slow down', data: {} }; let responses: Responses; beforeEach(() => { responses = new Responses(new Lacuna()); }); test('isBlockingError only matches the recoverable codes', () => { expect(isBlockingError(undefined)).toBe(false); expect(isBlockingError({ code: 1002, message: 'nope', data: {} })).toBe(false); expect(isBlockingError(slowDown)).toBe(true); expect(isBlockingError({ code: 1016, message: 'captcha', data: {} })).toBe(true); }); test('subscribe returns a working unsubscribe', async () => { const handler = jest.fn(); const unsubscribe = responses.subscribe(handler); await responses.dispatchBlocking(eventFor({ error: slowDown })); expect(handler).toHaveBeenCalledTimes(1); unsubscribe(); await responses.dispatchBlocking(eventFor({ error: slowDown })); expect(handler).toHaveBeenCalledTimes(1); }); test('dispatchBlocking awaits handlers in registration order', async () => { const order: string[] = []; responses.subscribe(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); order.push('first'); }); responses.subscribe(() => { order.push('second'); }); await responses.dispatchBlocking(eventFor({ error: slowDown })); expect(order).toEqual(['first', 'second']); }); test('dispatchBlocking adopts a response returned by a handler', async () => { responses.subscribe(() => ({ result: { recovered: true } })); const response = await responses.dispatchBlocking(eventFor({ error: slowDown })); expect(response.result).toEqual({ recovered: true }); expect(response.error).toBeUndefined(); }); test('dispatchBlocking still reaches later handlers once the error is resolved', async () => { const later = jest.fn(); responses.subscribe(() => ({ result: { recovered: true } })); responses.subscribe(later); await responses.dispatchBlocking(eventFor({ error: slowDown })); // Subscribers are promised every response, so resolving the error must not // cut the dispatch short - but later handlers see the *resolved* response, // which is what stops a second recovery handler retrying redundantly. expect(later).toHaveBeenCalledTimes(1); expect(later.mock.calls[0][0].response).toEqual({ result: { recovered: true } }); }); test('dispatchBlocking keeps going when a handler throws', async () => { console.warn = jest.fn(); const after = jest.fn(); responses.subscribe(() => { throw new Error('handler blew up'); }); responses.subscribe(after); const response = await responses.dispatchBlocking(eventFor({ error: slowDown })); expect(after).toHaveBeenCalled(); expect(response.error).toBe(slowDown); }); test('dispatch publishes without waiting and survives a rejecting handler', async () => { // The failing handler below is reported via log.warn; silence it so the // expected warning isn't mistaken for a broken test. console.warn = jest.fn(); const seen: ResponseEvent[] = []; responses.subscribe(() => Promise.reject(new Error('nope'))); responses.subscribe((event) => { seen.push(event); }); responses.dispatch(eventFor({ result: { ok: 1 } })); expect(seen).toHaveLength(1); expect(seen[0].request.method).toBe('get_status'); // Let the rejected handler settle so it can't leak into another test. await Promise.resolve(); });