fql-toolkit
Version:
108 lines • 4.79 kB
JavaScript
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