Skip to content

Commit 62918cc

Browse files
author
Transcript-Fetch-Admin
committed
feat: core client with health check
1 parent c6c056b commit 62918cc

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

‎src/client.ts‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/** The main TranscriptFetch client. */
2+
3+
import { resolveConfig, type ClientOptions, type ResolvedConfig } from "./config";
4+
import { APIConnectionError, APITimeoutError } from "./errors";
5+
import type { Health } from "./models";
6+
import { Transcripts } from "./resources/transcripts";
7+
import { backoffMs, buildHeaders, isRetryable, newIdempotencyKey, parseEnvelope } from "./transport";
8+
9+
export interface RequestOptions {
10+
body?: unknown;
11+
idempotencyKey?: string;
12+
/** Send the Authorization header. Defaults to true. */
13+
auth?: boolean;
14+
/** Auto-generate an Idempotency-Key when one is not supplied. */
15+
idempotent?: boolean;
16+
}
17+
18+
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
19+
20+
/**
21+
* Client for the TranscriptFetch API.
22+
*
23+
* ```ts
24+
* import { TranscriptFetch } from "transcriptfetch";
25+
*
26+
* const tf = new TranscriptFetch("tf_live_..."); // or set TRANSCRIPTFETCH_API_KEY
27+
* const t = await tf.transcripts.video("dQw4w9WgXcQ");
28+
* console.log(t.text, t.usage?.balance);
29+
* ```
30+
*/
31+
export class TranscriptFetch {
32+
/** The transcripts resource: video, channel, playlist, search, batch. */
33+
readonly transcripts: Transcripts;
34+
private readonly config: ResolvedConfig;
35+
36+
constructor(apiKey?: string, options?: ClientOptions);
37+
constructor(options: ClientOptions);
38+
constructor(apiKeyOrOptions?: string | ClientOptions, options: ClientOptions = {}) {
39+
const opts: ClientOptions =
40+
typeof apiKeyOrOptions === "string"
41+
? { ...options, apiKey: apiKeyOrOptions }
42+
: (apiKeyOrOptions ?? {});
43+
this.config = resolveConfig(opts);
44+
this.transcripts = new Transcripts(this);
45+
}
46+
47+
/** Low-level request with the shared retry loop. Resources call this. */
48+
async request(method: string, path: string, options: RequestOptions = {}): Promise<Record<string, unknown>> {
49+
let key = options.idempotencyKey;
50+
if (options.idempotent && !key) key = newIdempotencyKey();
51+
const baseHeaders = buildHeaders(this.config, key, options.auth !== false);
52+
const hasBody = options.body != null;
53+
const headers = hasBody ? { ...baseHeaders, "Content-Type": "application/json" } : baseHeaders;
54+
55+
let attempt = 0;
56+
for (;;) {
57+
const controller = new AbortController();
58+
const timer = setTimeout(() => controller.abort(), this.config.timeout);
59+
let response: Response;
60+
try {
61+
response = await this.config.fetch(this.config.baseUrl + path, {
62+
method,
63+
headers,
64+
body: hasBody ? JSON.stringify(options.body) : undefined,
65+
signal: controller.signal,
66+
});
67+
} catch (err) {
68+
if (err instanceof Error && err.name === "AbortError") {
69+
throw new APITimeoutError(`Request timed out after ${this.config.timeout}ms`);
70+
}
71+
throw new APIConnectionError(err instanceof Error ? err.message : String(err));
72+
} finally {
73+
clearTimeout(timer);
74+
}
75+
76+
if (isRetryable(response.status) && attempt < this.config.maxRetries) {
77+
await sleep(backoffMs(attempt, response.headers.get("retry-after")));
78+
attempt += 1;
79+
continue;
80+
}
81+
return parseEnvelope(response);
82+
}
83+
}
84+
85+
/** Unauthenticated liveness probe. No credits used. */
86+
async health(): Promise<Health> {
87+
return (await this.request("GET", "/api/v1/health", { auth: false })) as unknown as Health;
88+
}
89+
}

0 commit comments

Comments
 (0)