UNPKG

fql-toolkit

Version:
108 lines 4.79 kB
import FQLError from './errors.js'; // --------------------------------------------------------------------------- // Token normalization helpers // --------------------------------------------------------------------------- function normalizeToken(raw) { return { token: raw.TOKEN, formName: raw['FORM-NAME'] }; } function denormalizeToken(t) { return { TOKEN: t.token, 'FORM-NAME': t.formName }; } function denormalizeFqlToken(t) { return t ? denormalizeToken(t) : null; } // --------------------------------------------------------------------------- // Raw response normalization // --------------------------------------------------------------------------- export function parseRaw(raw) { const message = raw.msg ?? raw.message ?? ''; const isError = raw.error === true && raw.success !== true; const fqlToken = raw.fql_token ? normalizeToken(raw.fql_token) : null; return { ok: !isError, data: isError ? null : (raw.output ?? []), error: isError ? message : null, fqlToken, message, }; } const READ_VERB = /^\s*(get|show|sql\s+select)\b/i; const isRetryableRead = (query) => READ_VERB.test(query); const isRetryableStatus = (status, body) => status === 502 || status === 503 || status === 504 || (status === 500 && /process timeout/i.test(body)); const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms)); const jitter = () => Math.floor(Math.random() * 100); export default class FQLHTTPClient { constructor({ url, getToken, organizationId, onAuthError, retry }) { this.url = url; this.getToken = getToken; this.organizationId = organizationId; this.onAuthError = onAuthError; this.retry = retry; } _headers() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getToken()}`, }; } async executeFQL(query, fqlToken = null, organizationId) { const orgId = organizationId ?? this.organizationId; const body = { code: query, token: this.getToken(), fql_token: denormalizeFqlToken(fqlToken), }; if (orgId !== undefined) { body.organization_id = orgId; } const canRetry = Boolean(this.retry) && isRetryableRead(query); const maxAttempts = canRetry ? (this.retry.retries ?? 2) + 1 : 1; const baseDelay = this.retry?.baseDelayMs ?? 200; const sleep = this.retry?.sleep ?? defaultSleep; for (let attempt = 1;; attempt++) { try { const res = await fetch(`${this.url}/api/run_code`, { method: 'POST', headers: this._headers(), body: JSON.stringify(body), }); if (res.status === 401 || res.status === 450) { this.onAuthError?.(); return parseRaw({ msg: 'SESSION_EXPIRED: invalid or expired token', error: true }); } if (!res.ok) { // Preserve the engine's response body instead of collapsing every // non-OK response to a bare status. CL-FQL's 10s guard replies with // the plain-text body "Process timeout"; without this the caller // (and glasnost/api) can't tell a real timeout from any other 5xx. let bodyText = ''; try { bodyText = typeof res.text === 'function' ? (await res.text()).trim() : ''; } catch { /* body unreadable — fall back to the status-only message */ } if (attempt < maxAttempts && isRetryableStatus(res.status, bodyText)) { await sleep(baseDelay * 2 ** (attempt - 1) + jitter()); continue; } const detail = bodyText ? `: ${bodyText}` : ''; return parseRaw({ msg: `HTTP ${res.status}: FQL query execution failed${detail}`, error: true }); } const data = (await res.json()); return parseRaw(data); } catch (err) { if (attempt < maxAttempts) { await sleep(baseDelay * 2 ** (attempt - 1) + jitter()); continue; } if (err instanceof FQLError) { return parseRaw({ msg: err.message, error: true }); } const message = err instanceof Error ? err.message : String(err); return parseRaw({ msg: `FQL execution failed: ${message}`, error: true }); } } } } //# sourceMappingURL=client.js.map