Skip to content

Instantly share code, notes, and snippets.

@rossnelson
Created April 2, 2025 00:16
Show Gist options
  • Save rossnelson/750b9be7750fef55b9dd4a11645df627 to your computer and use it in GitHub Desktop.
Save rossnelson/750b9be7750fef55b9dd4a11645df627 to your computer and use it in GitHub Desktop.
Configurable HTTP client
import { withRetries } from '../utils/retry';
import { poll } from '../utils/poll';
export interface OperationHandler<TParams = any, TResponse = any> {
(params: TParams): RequestConfig<TResponse>;
}
export interface RequestConfig<T = unknown> {
method: string;
path: string;
query?: Record<string, any>;
body?: any;
parseResponse?: (json: any) => T;
retry?: {
retries?: number;
initialDelay?: number;
factor?: number;
shouldRetry?: (e: unknown) => boolean;
};
poll?: {
interval?: number;
timeout?: number;
until: (result: T) => boolean;
};
}
export interface ClientConfig {
baseUrl: string;
headers?: Record<string, string>;
fetch?: typeof fetch;
}
export class TemporalError extends Error {
constructor(message: string, public meta: any) {
super(message);
this.name = 'TemporalError';
}
}
export class HttpClient<TOperations extends Record<string, OperationHandler>> {
private fetchFn: typeof fetch;
private operations: TOperations;
constructor(
operations: TOperations,
private config: ClientConfig
) {
this.fetchFn = config.fetch ?? fetch;
this.operations = operations;
for (const [key, handler] of Object.entries(operations)) {
(this as any)[key] = async (params: any) => {
const reqConfig = handler(params);
return this.execute(reqConfig);
};
}
}
private async execute<T>(requestConfig: RequestConfig<T>): Promise<T> {
const exec = () => withRetries(() => this.makeRequest(requestConfig), requestConfig.retry);
if (requestConfig.poll) {
return poll(exec, requestConfig.poll);
}
return exec();
}
private async makeRequest<T>(requestConfig: RequestConfig<T>): Promise<T> {
const url = new URL(requestConfig.path, this.config.baseUrl);
if (requestConfig.query) {
for (const [key, value] of Object.entries(requestConfig.query)) {
url.searchParams.append(key, String(value));
}
}
const request = new Request(url.toString(), {
method: requestConfig.method,
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: requestConfig.body ? JSON.stringify(requestConfig.body) : undefined,
});
const response = await this.fetchFn(request);
if (!response.ok) {
throw new TemporalError(`${response.status}: request failed.`, {
request,
response,
});
}
const json = await response.json();
return requestConfig.parseResponse ? requestConfig.parseResponse(json) : (json as T);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment