@foss.global/codefeed 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import type * as transport from './forgeinventory.transport.js';
2
+ import * as validation from './forgeinventory.validation.js';
3
+
4
+ export interface IForgeRequestMetrics {
5
+ requests: number;
6
+ responseBytes: number;
7
+ rateLimitedRequests: number;
8
+ }
9
+
10
+ export interface IForgeRequestClientOptions {
11
+ transport: transport.TForgeTransport;
12
+ /** Credential headers, sent with every request and never reported. */
13
+ headers: Record<string, string>;
14
+ requestTimeoutMs: number;
15
+ maxResponseBytes: number;
16
+ }
17
+
18
+ export interface IForgeRequestOptions {
19
+ signal?: AbortSignal;
20
+ /** Optional lower response-body budget for this request. */
21
+ maxResponseBytes?: number;
22
+ /** Additional request headers, such as a provider's impersonation header. */
23
+ headers?: Record<string, string>;
24
+ }
25
+
26
+ export interface IForgeJsonResponse {
27
+ body: unknown;
28
+ headers: transport.IForgeTransportResponse['headers'];
29
+ }
30
+
31
+ /**
32
+ * One bounded, credential-scoped JSON GET at a time: a per-request deadline, a
33
+ * response-size budget and a strict JSON content type. Failures surface as
34
+ * `ForgeInventoryError` codes that never contain bodies, URLs or credentials.
35
+ */
36
+ export class ForgeRequestClient {
37
+ readonly #options: IForgeRequestClientOptions;
38
+ readonly #metrics: IForgeRequestMetrics = { requests: 0, responseBytes: 0, rateLimitedRequests: 0 };
39
+
40
+ constructor(optionsArg: IForgeRequestClientOptions) {
41
+ this.#options = { ...optionsArg, headers: { ...optionsArg.headers } };
42
+ }
43
+
44
+ public get metrics(): IForgeRequestMetrics { return { ...this.#metrics }; }
45
+
46
+ public async request(url: URL, requestArg: IForgeRequestOptions = {}): Promise<IForgeJsonResponse> {
47
+ const { signal } = requestArg;
48
+ const maxBytes = requestArg.maxResponseBytes ?? this.#options.maxResponseBytes;
49
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) throw new validation.ForgeInventoryError('invalid-response-budget');
50
+ const controller = new AbortController();
51
+ const abort = () => controller.abort();
52
+ if (signal?.aborted) throw new validation.ForgeInventoryError('request-aborted');
53
+ signal?.addEventListener('abort', abort, { once: true });
54
+ const timeout = setTimeout(abort, this.#options.requestTimeoutMs);
55
+ const deadline = Date.now() + this.#options.requestTimeoutMs;
56
+ let bodyChunks: AsyncIterator<Uint8Array> | undefined;
57
+ try {
58
+ this.#metrics.requests++;
59
+ const headers = { accept: 'application/json', ...requestArg.headers, ...this.#options.headers };
60
+ const response = await this.#options.transport(url, headers, controller.signal);
61
+ bodyChunks = response.body?.[Symbol.asyncIterator]();
62
+ if (response.status === 429) this.#metrics.rateLimitedRequests++;
63
+ if (response.status !== 200) throw new validation.ForgeInventoryError(`http-${response.status}`);
64
+ if (response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json' || !bodyChunks) throw new validation.ForgeInventoryError('invalid-content-type');
65
+ const chunks: Uint8Array[] = [];
66
+ let bytes = 0;
67
+ while (true) {
68
+ const result = await bodyChunks.next();
69
+ if (result.done) break;
70
+ bytes += result.value.byteLength; this.#metrics.responseBytes += result.value.byteLength;
71
+ if (bytes > Math.min(maxBytes, this.#options.maxResponseBytes)) throw new validation.ForgeInventoryError('response-size-limit');
72
+ if (controller.signal.aborted || Date.now() >= deadline) throw new validation.ForgeInventoryError('request-aborted-or-timed-out');
73
+ chunks.push(result.value);
74
+ }
75
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
76
+ const body = JSON.parse(text) as unknown;
77
+ if (controller.signal.aborted || Date.now() >= deadline) throw new validation.ForgeInventoryError('request-aborted-or-timed-out');
78
+ return { body, headers: response.headers };
79
+ } catch (error) {
80
+ if (error instanceof validation.ForgeInventoryError) throw error;
81
+ throw new validation.ForgeInventoryError(controller.signal.aborted ? 'request-aborted-or-timed-out' : 'request-or-json-failed');
82
+ } finally {
83
+ controller.abort(); clearTimeout(timeout); signal?.removeEventListener('abort', abort);
84
+ // Cancels an unread or partly read body; a completely read one has already released its connection.
85
+ try { await bodyChunks?.return?.(); } catch { /* The request has already been aborted. */ }
86
+ }
87
+ }
88
+ }
package/ts/index.ts CHANGED
@@ -487,3 +487,4 @@ export class CodeFeed {
487
487
  }
488
488
  export * from './classes.forgeinventory.js';
489
489
  export * from './classes.forgeinventoryscanner.js';
490
+ export * from './classes.giteasourceinventory.js';