@graphql-hive/core
Version:
173 lines (172 loc) • 6.53 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAgent = createAgent;
const version_js_1 = require("../version.js");
const http_client_js_1 = require("./http-client.js");
const utils_js_1 = require("./utils.js");
const defaultCircuitBreakerConfiguration = {
errorThresholdPercentage: 50,
volumeThreshold: 10,
resetTimeout: 30000,
};
function createAgent(pluginOptions, { data, body, headers = () => ({}), }) {
var _a;
const options = Object.assign(Object.assign({ timeout: 30000, enabled: true, minTimeout: 200, maxRetries: 3, sendInterval: 10000, maxSize: 25, name: 'hive-client', version: version_js_1.version }, pluginOptions), { circuitBreaker: pluginOptions.circuitBreaker == null || pluginOptions.circuitBreaker === true
? defaultCircuitBreakerConfiguration
: pluginOptions.circuitBreaker === false
? null
: pluginOptions.circuitBreaker });
const logger = (0, utils_js_1.createHiveLogger)((_a = pluginOptions.logger) !== null && _a !== void 0 ? _a : console, '[agent]');
const enabled = options.enabled !== false;
let timeoutID = null;
function schedule() {
if (timeoutID) {
clearTimeout(timeoutID);
}
timeoutID = setTimeout(send, options.sendInterval);
}
let scheduled = false;
let inProgressCaptures = [];
function capture(event) {
if (event instanceof Promise) {
const promise = captureAsync(event);
inProgressCaptures.push(promise);
void promise.finally(() => {
inProgressCaptures = inProgressCaptures.filter(p => p !== promise);
});
}
else {
captureSync(event);
}
}
async function captureAsync(event) {
captureSync(await event);
}
function captureSync(event) {
// Calling capture starts the schedule
if (!scheduled) {
scheduled = true;
schedule();
}
data.set(event);
if (data.size() >= options.maxSize) {
logger.debug('Sending immediately');
setImmediate(() => send({ throwOnError: false, skipSchedule: true }));
}
}
function sendImmediately(event) {
data.set(event);
logger.debug('Sending immediately');
return send({ throwOnError: true, skipSchedule: true });
}
async function sendHTTPCall(buffer) {
const signal = breaker.getSignal();
return await http_client_js_1.http.post(options.endpoint, buffer, {
headers: Object.assign({ accept: 'application/json', 'content-type': 'application/json', Authorization: `Bearer ${options.token}`, 'User-Agent': `${options.name}/${options.version}` }, headers()),
timeout: options.timeout,
retry: {
retries: options.maxRetries,
factor: 2,
},
logger,
fetchImplementation: pluginOptions.fetch,
signal,
});
}
async function send(sendOptions) {
if (!data.size() || !enabled) {
if (!(sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.skipSchedule)) {
schedule();
}
return null;
}
const buffer = await body();
const dataToSend = data.size();
data.clear();
logger.debug(`Sending report (queue ${dataToSend})`);
const response = sendFromBreaker(buffer)
.then(res => {
logger.debug(`Report sent!`);
return res;
})
.catch(error => {
logger.debug(`Failed to send report.`);
if (sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.throwOnError) {
throw error;
}
return null;
})
.finally(() => {
if (!(sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.skipSchedule)) {
schedule();
}
});
return response;
}
async function dispose() {
logger.debug('Disposing');
if (timeoutID) {
clearTimeout(timeoutID);
}
if (inProgressCaptures.length) {
await Promise.all(inProgressCaptures);
}
await send({
skipSchedule: true,
throwOnError: false,
});
}
let breaker;
let loadCircuitBreakerPromise = null;
const breakerLogger = (0, utils_js_1.createHiveLogger)(logger, '[circuit breaker]');
function noopBreaker() {
return {
getSignal() {
return undefined;
},
fire: sendHTTPCall,
};
}
if (options.circuitBreaker) {
/**
* We support Cloudflare, which does not has the `events` module.
* So we lazy load opossum which has `events` as a dependency.
*/
breakerLogger.info('initialize circuit breaker');
loadCircuitBreakerPromise = (0, utils_js_1.loadCircuitBreaker)(CircuitBreaker => {
breakerLogger.info('started');
const realBreaker = new CircuitBreaker(sendHTTPCall, Object.assign(Object.assign({}, options.circuitBreaker), { timeout: false, autoRenewAbortController: true }));
realBreaker.on('open', () => breakerLogger.error('circuit opened - backend seems unreachable.'));
realBreaker.on('halfOpen', () => breakerLogger.info('circuit half open - testing backend connectivity'));
realBreaker.on('close', () => breakerLogger.info('circuit closed - backend recovered '));
// @ts-expect-error missing definition in typedefs for `opposum`
breaker = realBreaker;
}, () => {
breakerLogger.info('circuit breaker not supported on platform');
breaker = noopBreaker();
});
}
else {
breaker = noopBreaker();
}
async function sendFromBreaker(...args) {
if (!breaker) {
await loadCircuitBreakerPromise;
}
try {
return await breaker.fire(...args);
}
catch (err) {
if (err instanceof Error && 'code' in err && err.code === 'EOPENBREAKER') {
breakerLogger.info('circuit open - sending report skipped');
return null;
}
throw err;
}
}
return {
capture,
sendImmediately,
dispose,
};
}