import Lacuna from '../lacuna'; // End-to-end cover for the recovery path, with the transport stubbed rather // than the handler: a real Server#call publishes to a real Responses bus, // which runs the real built-in handler, whose retry() re-enters Server#call. // The unit tests either side of this one stub the pieces in the middle. // // The network is stubbed because the live server's rpc_limit is 10,000 // requests/minute - genuinely tripping it would mean hammering a shared // server with five figures of traffic. const slowDown = { code: 1010, message: 'Slow down', data: {} }; /** Makes the transport answer with `failures` RPC limit errors, then succeed. */ function stubTransport(lacuna: Lacuna, failures: number) { let calls = 0; const post = jest.fn(async () => { calls += 1; if (calls <= failures) return { error: { id: 1, jsonrpc: '2.0', error: slowDown } }; return { data: { id: 1, jsonrpc: '2.0', result: { attempt: calls } } }; }); lacuna.server.client = { POST: post } as unknown as typeof lacuna.server.client; return post; } let lacuna: Lacuna; beforeEach(() => { console.warn = jest.fn(); lacuna = new Lacuna(); lacuna.session.set('test-session'); }); test('an RPC limit error is retried until it succeeds, transparently to the caller', async () => { const post = stubTransport(lacuna, 2); lacuna.enableRpcLimitHandler({ baseDelayMs: 1 }); const { result, error } = await lacuna.empire.getStatus(); expect(post).toHaveBeenCalledTimes(3); expect(error).toBeUndefined(); expect(result).toEqual({ attempt: 3 }); }); test('gives up after maxRetries and hands the error back to the caller', async () => { const post = stubTransport(lacuna, Infinity); lacuna.enableRpcLimitHandler({ maxRetries: 2, baseDelayMs: 1 }); const { result, error } = await lacuna.empire.getStatus(); // Original call plus two retries. expect(post).toHaveBeenCalledTimes(3); expect(result).toBeUndefined(); expect(error).toEqual(slowDown); }); test('without the handler enabled the RPC limit error is returned as-is', async () => { const post = stubTransport(lacuna, 2); const { error } = await lacuna.empire.getStatus(); expect(post).toHaveBeenCalledTimes(1); expect(error).toEqual(slowDown); }); test('disabling the handler stops it retrying', async () => { const post = stubTransport(lacuna, 2); const disable = lacuna.enableRpcLimitHandler({ baseDelayMs: 1 }); disable(); const { error } = await lacuna.empire.getStatus(); expect(post).toHaveBeenCalledTimes(1); expect(error).toEqual(slowDown); }); test('an observing subscriber still sees every attempt, even the recovered ones', async () => { stubTransport(lacuna, 2); lacuna.enableRpcLimitHandler({ baseDelayMs: 1 }); const attempts: number[] = []; lacuna.onResponse((event) => { attempts.push(event.attempt); }); await lacuna.empire.getStatus(); // Retries nest: attempt 0's handler is still awaiting attempt 1's, which is // awaiting attempt 2's, so the innermost attempt settles - and reaches this // subscriber - first. All three arrive, which is the point. expect(attempts).toEqual([2, 1, 0]); }); test('the retry depth backstop bounds a handler that always retries', async () => { const post = stubTransport(lacuna, Infinity); lacuna.onResponse((event) => (event.response.error ? event.retry() : undefined)); const { error } = await lacuna.empire.getStatus(); // MAX_RETRY_DEPTH is 10, so the original call plus 10 retries, then stop. expect(post).toHaveBeenCalledTimes(11); expect(error).toEqual(slowDown); });