Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
n1ru4l committed Jul 19, 2024
1 parent 9240b57 commit f2bba49
Show file tree
Hide file tree
Showing 15 changed files with 707 additions and 651 deletions.
147 changes: 44 additions & 103 deletions packages/libraries/core/src/client/agent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import retry from 'async-retry';
import { version } from '../version.js';
import { http } from './http-client.js';
import type { Logger } from './types.js';

type ReadOnlyResponse = Pick<Response, 'status' | 'text' | 'json'>;
type ReadOnlyResponse = Pick<Response, 'status' | 'text' | 'json' | 'statusText'>;

export interface AgentOptions {
enabled?: boolean;
Expand Down Expand Up @@ -55,12 +54,10 @@ export interface AgentOptions {
export function createAgent<TEvent>(
pluginOptions: AgentOptions,
{
prefix,
data,
body,
headers = () => ({}),
}: {
prefix: string;
data: {
clear(): void;
set(data: TEvent): void;
Expand Down Expand Up @@ -97,10 +94,14 @@ export function createAgent<TEvent>(

function debugLog(msg: string) {
if (options.debug) {
options.logger.info(`[hive][${prefix}]${enabled ? '' : '[DISABLED]'} ${msg}`);
options.logger.info(msg);
}
}

function errorLog(msg: string) {
options.logger.error(msg);
}

let scheduled = false;
let inProgressCaptures: Promise<void>[] = [];

Expand Down Expand Up @@ -132,118 +133,59 @@ export function createAgent<TEvent>(

if (data.size() >= options.maxSize) {
debugLog('Sending immediately');
setImmediate(() => send({ runOnce: true, throwOnError: false }));
setImmediate(() => send({ throwOnError: false }));
}
}

function sendImmediately(event: TEvent): Promise<ReadOnlyResponse | null> {
data.set(event);

debugLog('Sending immediately');
return send({ runOnce: true, throwOnError: true });
return send({ throwOnError: true });
}

async function send(sendOptions: {
runOnce?: boolean;
throwOnError: true;
}): Promise<ReadOnlyResponse | null>;
async function send(sendOptions: {
runOnce?: boolean;
throwOnError: false;
}): Promise<ReadOnlyResponse | null>;
async function send(sendOptions?: {
runOnce?: boolean;
throwOnError: boolean;
}): Promise<ReadOnlyResponse | null> {
const runOnce = sendOptions?.runOnce ?? false;

if (!data.size()) {
if (!runOnce) {
schedule();
}
async function send(sendOptions?: { throwOnError?: boolean }): Promise<ReadOnlyResponse | null> {
if (!data.size() || !enabled) {
return null;
}

try {
const buffer = await body();
const dataToSend = data.size();

data.clear();

const sendReport: retry.RetryFunction<{
status: number;
text(): Promise<string>;
json(): Promise<unknown>;
}> = async (_bail, attempt) => {
debugLog(`Sending (queue ${dataToSend}) (attempt ${attempt})`);

if (!enabled) {
return {
status: 200,
text: async () => 'OK',
json: async () => ({}),
};
}

const response = await http
.post(options.endpoint, buffer, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'User-Agent': `${options.name}/${version}`,
...headers(),
},
timeout: options.timeout,
fetchImplementation: pluginOptions.__testing?.fetch,
logger: options.logger,
})
.catch(error => {
debugLog(`Attempt ${attempt} failed: ${error.message}`);
return Promise.reject(error);
});

if (response.status >= 200 && response.status < 300) {
return response;
const buffer = await body();
const dataToSend = data.size();

data.clear();

debugLog(`Sending report (queue ${dataToSend})`);
const response = await http
.post(options.endpoint, buffer, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
Authorization: `Bearer ${options.token}`,
'User-Agent': `${options.name}/${version}`,
...headers(),
},
timeout: options.timeout,
retry: {
retries: options.maxRetries,
factor: 2,
},
logger: options.logger,
fetchImplementation: pluginOptions.__testing?.fetch,
})
.then(res => {
debugLog(`Report sent!`);
return res;
})
.catch(error => {
errorLog(`Failed to send report.`);

if (sendOptions?.throwOnError) {
throw error;
}

debugLog(`Attempt ${attempt} failed: ${response.status}`);
throw new Error(`${response.status}: ${response.statusText} ${await response.text()}`);
};

const response = await retry(sendReport, {
retries: options.maxRetries,
minTimeout: options.minTimeout,
factor: 2,
return null;
});

if (response.status < 200 || response.status >= 300) {
throw new Error(
`[hive][${prefix}] POST ${options.endpoint} failed with status code ${response.status}. ${await response.text()}`,
);
}

debugLog(`Sent!`);

if (!runOnce) {
schedule();
}
return response;
} catch (error: any) {
if (!runOnce) {
schedule();
}

if (sendOptions?.throwOnError) {
throw error;
}

options.logger.error(
`[hive][${prefix}] POST ${options.endpoint} failed with status ${error.message}`,
);

return null;
}
return response;
}

async function dispose() {
Expand All @@ -257,7 +199,6 @@ export function createAgent<TEvent>(
}

await send({
runOnce: true,
throwOnError: false,
});
}
Expand Down
35 changes: 18 additions & 17 deletions packages/libraries/core/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ import { createPersistedDocuments } from './persisted-documents.js';
import { createReporting } from './reporting.js';
import type { HiveClient, HivePluginOptions } from './types.js';
import { createUsage } from './usage.js';
import { logIf } from './utils.js';
import { createHiveLogger, logIf } from './utils.js';

export function createHive(options: HivePluginOptions): HiveClient {
const logger = options?.agent?.logger ?? console;
const logger = createHiveLogger(options?.agent?.logger ?? console, '[hive]');
let enabled = options.enabled ?? true;

if (enabled === false) {
logIf(options.debug === true, '[hive] is not enabled.', logger.info);
logIf(options.debug === true, 'plugin is not enabled.', logger.info);
}

if (!options.token && enabled) {
enabled = false;
logger.info('[hive] Missing token, disabling.');
logger.info('Missing token, disabling.');
}

const mergedOptions: HivePluginOptions = { ...options, enabled } as HivePluginOptions;
Expand Down Expand Up @@ -50,6 +50,7 @@ export function createHive(options: HivePluginOptions): HiveClient {
const printTokenInfo = enabled
? options.printTokenInfo === true || (!!options.debug && options.printTokenInfo !== false)
: false;
const infoLogger = createHiveLogger(logger, '[info]');

const info = printTokenInfo
? async () => {
Expand Down Expand Up @@ -97,6 +98,8 @@ export function createHive(options: HivePluginOptions): HiveClient {
}
`;

infoLogger.info('Fetching token details...');

const response = await http.post(
endpoint,
JSON.stringify({
Expand All @@ -113,7 +116,7 @@ export function createHive(options: HivePluginOptions): HiveClient {
},
timeout: 30_000,
fetchImplementation: options?.agent?.__testing?.fetch,
logger,
logger: infoLogger,
},
);

Expand Down Expand Up @@ -145,9 +148,9 @@ export function createHive(options: HivePluginOptions): HiveClient {
const projectUrl = `${organizationUrl}/${project.cleanId}`;
const targetUrl = `${projectUrl}/${target.cleanId}`;

logger.info(
infoLogger.info(
[
'[hive][info] Token details',
'Token details',
'',
`Token name: ${print(tokenInfo.token.name)}`,
`Organization: ${print(organization.name, organizationUrl)}`,
Expand All @@ -161,23 +164,21 @@ export function createHive(options: HivePluginOptions): HiveClient {
].join('\n'),
);
} else if (result.data?.tokenInfo.message) {
logger.error(
`[hive][info] Token not found. Reason: ${result.data?.tokenInfo.message}`,
);
logger.info(
`[hive][info] How to create a token? https://docs.graphql-hive.com/features/tokens`,
infoLogger.error(`Token not found. Reason: ${result.data?.tokenInfo.message}`);
infoLogger.info(
`How to create a token? https://docs.graphql-hive.com/features/tokens`,
);
} else {
logger.error(`[hive][info] ${result.errors![0].message}`);
logger.info(
`[hive][info] How to create a token? https://docs.graphql-hive.com/features/tokens`,
infoLogger.error(`${result.errors![0].message}`);

Check failure on line 172 in packages/libraries/core/src/client/client.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

use `String(result.errors![0].message)` instead
infoLogger.info(
`How to create a token? https://docs.graphql-hive.com/features/tokens`,
);
}
} else {
logger.error(`[hive][info] Error ${response.status}: ${response.statusText}`);
infoLogger.error(`Error ${response.status}: ${response.statusText}`);
}
} catch (error) {
logger.error(`[hive][info] Error ${(error as Error)?.message ?? error}`);
infoLogger.error(`Error ${(error as Error)?.message ?? error}`);
}
}
: () => {};
Expand Down
11 changes: 5 additions & 6 deletions packages/libraries/core/src/client/gateways.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { version } from '../version.js';
import { http } from './http-client.js';
import type { SchemaFetcherOptions, ServicesFetcherOptions } from './types.js';
import type { Logger, SchemaFetcherOptions, ServicesFetcherOptions } from './types.js';

Check failure on line 3 in packages/libraries/core/src/client/gateways.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

'Logger' is defined but never used. Allowed unused vars must match /^_/u
import { createHash, joinUrl } from './utils.js';

interface Schema {
Expand All @@ -10,6 +10,7 @@ interface Schema {
}

function createFetcher(options: SchemaFetcherOptions & ServicesFetcherOptions) {
const logger = options.logger ?? console;
let cacheETag: string | null = null;
let cached: {
id: string;
Expand Down Expand Up @@ -37,12 +38,12 @@ function createFetcher(options: SchemaFetcherOptions & ServicesFetcherOptions) {
.get(endpoint, {
headers,
retry: {
retryWhen: response => response.status >= 500,
okWhen: response => response.status === 304,
retries: 10,
maxTimeout: 200,
minTimeout: 1,
},
isRequestOk: response => response.ok || response.status === 304,
logger,
})
.then(async response => {
if (response.ok) {
Expand All @@ -61,9 +62,7 @@ function createFetcher(options: SchemaFetcherOptions & ServicesFetcherOptions) {
return cached;
}

throw new Error(
`Failed to GET ${endpoint}, received: ${response.status} ${response.statusText ?? 'Internal Server Error'}`,
);
throw new Error(`Unexpected error.`);
});
};
}
Expand Down
Loading

0 comments on commit f2bba49

Please sign in to comment.