import fs from 'node:fs'; import { Lacuna, SESSION_EXPIRED_ERROR_CODE } from '../'; import type { ResponseHandler } from '../'; import { tmpfile } from './tmpfile'; // The live server rate-limits to 200 requests/minute per empire. With one // Jest test *file* per endpoint module, having every file log in separately // burns a big chunk of that budget on nothing but auth. A session is cheap // to reuse across the whole test run, so it's cached on disk (keyed to this // account) and only refreshed once it's older than SESSION_TTL_MS. const CACHE_FILE = tmpfile('tle-client-test-session.json'); const SESSION_TTL_MS = 4 * 60 * 1000; const credentials = () => ({ name: process.env.LACUNA_TEST_EMPIRE_NAME, password: process.env.LACUNA_TEST_EMPIRE_PASSWORD, }); // After a successful re-login, treat any 1006 that lands within this window as // "not actually a dead session" (e.g. the v2 layer's arg-mapping quirk that // rejects a valid session on some named-only methods) and leave it alone // rather than re-logging in on a loop. const REFRESH_COOLDOWN_MS = 10 * 1000; interface CachedSession { sessionId: string; expiresAt: number; } function readCachedSessionId(): string | undefined { try { const cached = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')) as CachedSession; if (cached.expiresAt > Date.now()) return cached.sessionId; } catch { // No cache yet, or it's corrupt/unreadable - fall through to a fresh login. } return undefined; } function writeCachedSessionId(sessionId: string) { const cached: CachedSession = { sessionId, expiresAt: Date.now() + SESSION_TTL_MS }; fs.writeFileSync(CACHE_FILE, JSON.stringify(cached)); } function clearCachedSession() { try { fs.rmSync(CACHE_FILE, { force: true }); } catch { // Nothing to clear, or it's not ours to remove - the re-login below will // overwrite it anyway. } } async function authenticate(lacuna: Lacuna): Promise { const { name, password } = credentials(); await lacuna.authenticate(name, password); const sessionId = lacuna.session.get(); if (sessionId) writeCachedSessionId(sessionId); return sessionId || undefined; } // A dead cached session hands the same 1006 to every test that uses it. The // first one to hit it does the re-login; concurrent calls (a `Promise.all` of // endpoint calls) await that same attempt instead of each firing their own. let reloginInFlight: Promise | undefined; let lastRefreshAt = 0; async function refreshSession(lacuna: Lacuna): Promise { clearCachedSession(); const sessionId = await authenticate(lacuna); if (sessionId) lastRefreshAt = Date.now(); return sessionId; } /** * Recovers from a rejected-session error (1006): clears the on-disk session * cache, logs in again, stores the new session, and retries the failing call * so the test that triggered it carries on. 1006 is a blocking error code, so * the retry's response is the one the original caller receives. */ const sessionExpiredHandler = (lacuna: Lacuna): ResponseHandler => async (event) => { if (event.response.error?.code !== SESSION_EXPIRED_ERROR_CODE) return; if (event.attempt >= 1) return; // already retried once on a fresh session - don't loop if (Date.now() - lastRefreshAt < REFRESH_COOLDOWN_MS) return; // just re-logged in; not a dead session reloginInFlight ??= refreshSession(lacuna).finally(() => { reloginInFlight = undefined; }); const sessionId = await reloginInFlight; if (!sessionId) return; // re-login failed - surface the original 1006 return event.retry(); }; interface GetLacunaOptions { /** * Turn on the built-in RPC-limit (1010) backoff/retry handler. Default true. * Pass false in a test that deliberately asserts a raw 1010 response - the * backend overloads that code for some "can't do that right now" refusals, * and the retry loop would otherwise swallow it (and blow the test timeout). */ handleRpcLimit?: boolean; } export const getLacuna = async ({ handleRpcLimit = true }: GetLacunaOptions = {}) => { const lacuna = new Lacuna(); // Ride out the server's per-minute request limit with a progressive backoff, // and recover transparently if the cached session has gone stale. if (handleRpcLimit) lacuna.enableRpcLimitHandler(); lacuna.onResponse(sessionExpiredHandler(lacuna)); const cachedSessionId = readCachedSessionId(); if (cachedSessionId) { lacuna.session.set(cachedSessionId); return lacuna; } await authenticate(lacuna); return lacuna; };