Something went wrong. Try again.
TypeScript client for accessing TLE Community.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125import 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<string | undefined> { 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<string | undefined> | undefined;let lastRefreshAt = 0;
async function refreshSession(lacuna: Lacuna): Promise<string | undefined> { 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;};