Created
April 2, 2025 00:16
-
-
Save rossnelson/750b9be7750fef55b9dd4a11645df627 to your computer and use it in GitHub Desktop.
Configurable HTTP client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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