UNPKG

@graphql-hive/core

Version:
162 lines (161 loc) • 7.05 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.URL = exports.http = void 0; exports.makeFetchCall = makeFetchCall; const tslib_1 = require("tslib"); const async_retry_1 = tslib_1.__importDefault(require("async-retry")); const logger_1 = require("@graphql-hive/logger"); const signal_1 = require("@graphql-hive/signal"); const fetch_1 = require("@whatwg-node/fetch"); Object.defineProperty(exports, "URL", { enumerable: true, get: function () { return fetch_1.URL; } }); function get(endpoint, config) { return makeFetchCall(endpoint, { method: 'GET', headers: config.headers, timeout: config.timeout, retry: config.retry, fetchImplementation: config.fetchImplementation, logger: config.logger, isRequestOk: config.isRequestOk, }); } function post(endpoint, data, config) { return makeFetchCall(endpoint, Object.assign({ body: data, method: 'POST' }, config)); } exports.http = { get, post, }; function chooseLogger(logger) { if (!logger) { return new logger_1.Logger({ writers: [{ write() { } }], }); } if (logger instanceof logger_1.Logger) { return logger; } return new logger_1.Logger({ level: 'debug', writers: [ { write(level, _attrs, msg) { if (level === 'debug' && logger.debug && msg) { logger.debug(msg); return; } if (level === 'info' && msg) { logger.info(msg); return; } if (level === 'error' && msg) { logger.error(msg); } }, }, ], }); } async function makeFetchCall(endpoint, config) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const logger = chooseLogger(config.logger); const isRequestOk = (_a = config.isRequestOk) !== null && _a !== void 0 ? _a : (response => response.ok); let retries = 0; let minTimeout = 200; let maxTimeout = 2000; let factor = 1.2; const actionHeader = config.method === 'POST' ? { 'x-client-action-id': fetch_1.crypto.randomUUID() } : undefined; if (config.retry !== false) { retries = (_c = (_b = config.retry) === null || _b === void 0 ? void 0 : _b.retries) !== null && _c !== void 0 ? _c : 5; minTimeout = (_e = (_d = config.retry) === null || _d === void 0 ? void 0 : _d.minTimeout) !== null && _e !== void 0 ? _e : 200; maxTimeout = (_g = (_f = config.retry) === null || _f === void 0 ? void 0 : _f.maxTimeout) !== null && _g !== void 0 ? _g : 2000; factor = (_j = (_h = config.retry) === null || _h === void 0 ? void 0 : _h.factor) !== null && _j !== void 0 ? _j : 1.2; } return await (0, async_retry_1.default)(async (bail, attempt) => { var _a, _b, _c, _d, _e; const isFinalAttempt = attempt > retries; const requestId = fetch_1.crypto.randomUUID(); logger.debug(`${config.method} ${endpoint} (x-request-id=${requestId})` + (retries > 0 ? ' ' + getAttemptMessagePart(attempt, retries + 1) : '')); const getDuration = measureTime(); const timeoutSignal = AbortSignal.timeout((_a = config.timeout) !== null && _a !== void 0 ? _a : 20000); const signal = config.signal ? (0, signal_1.abortSignalAny)([config.signal, timeoutSignal]) : timeoutSignal; const response = await ((_b = config.fetchImplementation) !== null && _b !== void 0 ? _b : fetch_1.fetch)(endpoint, { method: config.method, body: config.body, headers: Object.assign(Object.assign({ 'x-request-id': requestId }, actionHeader), config.headers), signal, }).catch((error) => { const msg = `${config.method} ${endpoint} (x-request-id=${requestId}) failed ${getDuration()}. ` + getErrorMessage(error); if (isFinalAttempt) { logger.error({ error }, msg); } else { logger.debug({ error }, msg); } throw new Error(`Unexpected HTTP error. (x-request-id=${requestId})`, { cause: error }); }); if (((_c = config.signal) === null || _c === void 0 ? void 0 : _c.aborted) === true) { const error = (_d = config.signal.reason) !== null && _d !== void 0 ? _d : new Error('Request aborted externally.'); bail(error); throw error; } if (isRequestOk(response)) { (_e = logger === null || logger === void 0 ? void 0 : logger.debug) === null || _e === void 0 ? void 0 : _e.call(logger, `${config.method} ${endpoint} (x-request-id=${requestId}) succeeded with status ${response.status} ${getDuration()}.`); return response; } if (isFinalAttempt) { logger.error(`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status} ${getDuration()}: ${(await response.text()) || '<empty response body>'}`); logger.error(`${config.method} ${endpoint} (x-request-id=${requestId}) retry limit exceeded after ${attempt} attempts.`); } else { logger.debug(`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status} ${getDuration()}: ${(await response.text()) || '<empty response body>'}`); } const error = new Error(`${config.method} ${endpoint} (x-request-id=${requestId}) failed with status ${response.status}.`); if (response.status >= 400 && response.status < 500) { if (retries > 0) { logger.error(`Abort retry because of status code ${response.status}.`); } bail(error); } throw error; }, { retries, minTimeout, maxTimeout, factor, }); } function getErrorMessage(error) { if (error && typeof error === 'object' && 'message' in error) { return String(error.message); } return '<no error message>'; } function getAttemptMessagePart(attempt, retry) { return `Attempt (${attempt}/${retry})`; } function measureTime() { const start = Date.now(); return () => '(' + formatTimestamp(Date.now() - start) + ')'; } function formatTimestamp(timestamp) { const milliseconds = timestamp % 1000; const seconds = Math.floor((timestamp / 1000) % 60); const minutes = Math.floor((timestamp / (1000 * 60)) % 60); const hours = Math.floor(timestamp / (1000 * 60 * 60)); const parts = []; if (hours > 0) { parts.push(`${hours}h`); } if (minutes > 0 || hours > 0) { // Include minutes if hours exist, even if minutes are 0 parts.push(`${minutes}m`); } if (seconds > 0 || minutes > 0 || hours > 0) { parts.push(`${seconds}s`); } parts.push(`${milliseconds}ms`); return parts.join(':'); }