Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/core-internal/src/services/code-navigation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1605,13 +1605,28 @@ export class CodeNavigationServiceImpl implements CodeNavigationService {
userAgent?: string;
clientVersion?: string;
} = {},
private readonly retryConfig?: {
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
jitter?: boolean;
},
) {}

private async postGraphqlWithTargetResolutionFallback(input: {
token: string;
query: string;
variables: Record<string, unknown>;
}): Promise<PkgseerGraphqlResponse> {
const retryOptions = this.retryConfig
? {
maxRetries: this.retryConfig.maxRetries,
baseDelayMs: this.retryConfig.baseDelayMs,
maxDelayMs: this.retryConfig.maxDelayMs,
jitter: this.retryConfig.jitter,
}
: undefined;

const response = await postPkgseerGraphql({
endpointUrl: this.codeNavigationUrl,
token: input.token,
Expand All @@ -1620,6 +1635,7 @@ export class CodeNavigationServiceImpl implements CodeNavigationService {
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions,
});
if (response.status < 200 || response.status >= 300) return response;
if (!hasSchemaMismatchErrors(response.parsedBody)) return response;
Expand All @@ -1638,6 +1654,7 @@ export class CodeNavigationServiceImpl implements CodeNavigationService {
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions,
});
if (!hasSchemaMismatchErrors(fallbackResponse.parsedBody)) {
return fallbackResponse;
Expand Down
65 changes: 64 additions & 1 deletion packages/core-internal/src/services/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,71 @@ export function getCodeNavigationUrl(): string {
}

/**
* Get API token from environment variable (for CI/automation).
* API token from environment variable (for CI/automation).
*/
export function getEnvApiToken(): string | undefined {
return process.env.GITHITS_API_TOKEN;
}

// ---------------------------------------------------------------------------
// Retry configuration
// ---------------------------------------------------------------------------

export interface RetryConfig {
/** Maximum number of retry attempts (default: 3) */
maxRetries: number;
/** Base delay in milliseconds for exponential backoff (default: 1000) */
baseDelayMs: number;
/** Maximum delay in milliseconds (default: 30000) */
maxDelayMs: number;
/** Whether to add jitter to delay (default: true) */
jitter: boolean;
}

const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
jitter: true,
};

/**
* Get retry configuration with environment variable overrides.
*
* Environment variables:
* - `GITHITS_RETRY_MAX` — maximum retry attempts
* - `GITHITS_RETRY_BASE_DELAY_MS` — base delay in milliseconds
* - `GITHITS_RETRY_MAX_DELAY_MS` — maximum delay in milliseconds
* - `GITHITS_RETRY_JITTER` — enable/disable jitter ("true"/"false")
*/
export function getRetryConfig(): RetryConfig {
const maxRetries = parseEnvInt(
process.env.GITHITS_RETRY_MAX,
DEFAULT_RETRY_CONFIG.maxRetries,
);
const baseDelayMs = parseEnvInt(
process.env.GITHITS_RETRY_BASE_DELAY_MS,
DEFAULT_RETRY_CONFIG.baseDelayMs,
);
const maxDelayMs = parseEnvInt(
process.env.GITHITS_RETRY_MAX_DELAY_MS,
DEFAULT_RETRY_CONFIG.maxDelayMs,
);
const jitter = parseEnvBool(
process.env.GITHITS_RETRY_JITTER,
DEFAULT_RETRY_CONFIG.jitter,
);

return { maxRetries, baseDelayMs, maxDelayMs, jitter };
}

function parseEnvInt(value: string | undefined, fallback: number): number {
if (value === undefined) return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

function parseEnvBool(value: string | undefined, fallback: boolean): boolean {
if (value === undefined) return fallback;
return value.toLowerCase() === "true";
}
124 changes: 96 additions & 28 deletions packages/core-internal/src/services/githits-service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
DEFAULT_FETCH_TIMEOUT_MS,
fetchWithTimeout,
retryFetchWithTimeout,
} from "../shared/fetch-timeout.js";
import type { ClientHeaderBuilder } from "../shared/request-headers.js";
import { withTelemetrySpan } from "../shared/telemetry.js";
import type { RetryConfig } from "./config.js";

/**
* Neutral auth-required message for service/core errors. Surface layers append
Expand Down Expand Up @@ -100,6 +102,12 @@ export interface GitHitsServiceRuntimeOptions {
userAgent?: string;
}

/**
* Retry configuration for HTTP requests.
* Re-exported from config.ts for backward compatibility.
*/
export type { RetryConfig } from "./config.js";

/**
* Service interface for GitHits REST API.
*/
Expand Down Expand Up @@ -127,24 +135,52 @@ export class GitHitsServiceImpl implements GitHitsService {
private readonly fetchFn?: typeof fetch,
private readonly fetchTimeoutMs: number = DEFAULT_FETCH_TIMEOUT_MS,
private readonly runtime: GitHitsServiceRuntimeOptions = {},
private readonly retryConfig?: RetryConfig,
) {}

async search(params: SearchParams): Promise<string> {
return withTelemetrySpan("githits.search.request", async () => {
const response = await fetchWithTimeout(
`${this.apiUrl}/search`,
{
method: "POST",
headers: this.headers(),
body: JSON.stringify({
query: params.query,
language: params.language,
license_mode: params.licenseMode ?? "strict",
include_explanation: params.includeExplanation ?? false,
}),
},
this.fetchOptions(),
);
const fetchFn = async (): Promise<Response> => {
return fetchWithTimeout(
`${this.apiUrl}/search`,
{
method: "POST",
headers: this.headers(),
body: JSON.stringify({
query: params.query,
language: params.language,
license_mode: params.licenseMode ?? "strict",
include_explanation: params.includeExplanation ?? false,
}),
},
this.fetchOptions(),
);
};

let response: Response;
if (this.retryConfig) {
// Search POST is idempotent (same query = same result)
response = await retryFetchWithTimeout(
`${this.apiUrl}/search`,
{
method: "POST",
headers: this.headers(),
body: JSON.stringify({
query: params.query,
language: params.language,
license_mode: params.licenseMode ?? "strict",
include_explanation: params.includeExplanation ?? false,
}),
},
{
...this.fetchOptions(),
...this.retryConfig,
idempotent: true,
},
);
} else {
response = await fetchFn();
}

if (!response.ok) {
throw await this.createError(response);
Expand All @@ -156,13 +192,29 @@ export class GitHitsServiceImpl implements GitHitsService {

async getLanguages(): Promise<Language[]> {
return withTelemetrySpan("githits.languages.request", async () => {
const response = await fetchWithTimeout(
`${this.apiUrl}/languages`,
{
headers: this.headers(),
},
this.fetchOptions(),
);
let response: Response;
if (this.retryConfig) {
// Languages GET is idempotent
response = await retryFetchWithTimeout(
`${this.apiUrl}/languages`,
{
headers: this.headers(),
},
{
...this.fetchOptions(),
...this.retryConfig,
idempotent: true,
},
);
} else {
response = await fetchWithTimeout(
`${this.apiUrl}/languages`,
{
headers: this.headers(),
},
this.fetchOptions(),
);
}

if (!response.ok) {
throw await this.createError(response);
Expand All @@ -178,13 +230,29 @@ export class GitHitsServiceImpl implements GitHitsService {
query,
limit: String(limit),
});
const response = await fetchWithTimeout(
`${this.apiUrl}/languages?${params.toString()}`,
{
headers: this.headers(),
},
this.fetchOptions(),
);
let response: Response;
if (this.retryConfig) {
// Languages search GET is idempotent
response = await retryFetchWithTimeout(
`${this.apiUrl}/languages?${params.toString()}`,
{
headers: this.headers(),
},
{
...this.fetchOptions(),
...this.retryConfig,
idempotent: true,
},
);
} else {
response = await fetchWithTimeout(
`${this.apiUrl}/languages?${params.toString()}`,
{
headers: this.headers(),
},
this.fetchOptions(),
);
}

if (!response.ok) {
throw await this.createError(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1745,8 +1745,31 @@ export class PackageIntelligenceServiceImpl
userAgent?: string;
clientVersion?: string;
} = {},
private readonly retryConfig?: {
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
jitter?: boolean;
},
) {}

private getRetryOptions():
| {
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
jitter?: boolean;
}
| undefined {
if (!this.retryConfig) return undefined;
return {
maxRetries: this.retryConfig.maxRetries,
baseDelayMs: this.retryConfig.baseDelayMs,
maxDelayMs: this.retryConfig.maxDelayMs,
jitter: this.retryConfig.jitter,
};
}

async packageSummary(params: PackageSummaryParams): Promise<PackageSummary> {
return withTelemetrySpan("pkg-intel.summary.request", () =>
executeWithTokenRefresh({
Expand Down Expand Up @@ -1775,6 +1798,7 @@ export class PackageIntelligenceServiceImpl
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions: this.getRetryOptions(),
});
} catch (cause) {
if (cause instanceof PkgseerTransportError) {
Expand Down Expand Up @@ -2130,6 +2154,7 @@ export class PackageIntelligenceServiceImpl
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions: this.getRetryOptions(),
});
} catch (cause) {
if (cause instanceof PkgseerTransportError) {
Expand Down Expand Up @@ -2281,6 +2306,7 @@ export class PackageIntelligenceServiceImpl
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions: this.getRetryOptions(),
});
} catch (cause) {
if (cause instanceof PkgseerTransportError) {
Expand Down Expand Up @@ -2343,6 +2369,7 @@ export class PackageIntelligenceServiceImpl
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions: this.getRetryOptions(),
});
} catch (cause) {
if (cause instanceof PkgseerTransportError) {
Expand Down Expand Up @@ -2660,6 +2687,7 @@ export class PackageIntelligenceServiceImpl
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions: this.getRetryOptions(),
});
} catch (cause) {
if (cause instanceof PkgseerTransportError) {
Expand Down Expand Up @@ -2778,6 +2806,7 @@ export class PackageIntelligenceServiceImpl
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions: this.getRetryOptions(),
});
} catch (cause) {
if (cause instanceof PkgseerTransportError) {
Expand Down Expand Up @@ -2878,6 +2907,7 @@ export class PackageIntelligenceServiceImpl
fetchFn: this.fetchFn,
clientHeaders: this.runtime.clientHeaders,
userAgent: this.runtime.userAgent,
retryOptions: this.getRetryOptions(),
});
} catch (cause) {
if (cause instanceof PkgseerTransportError) {
Expand Down
Loading