/* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ import { HTTPClient, matchContentType, matchStatusCode, isAbortError, isTimeoutError, isConnectionError, } from "./http.js"; import { SecurityState } from "./security.js"; import { retry, RetryConfig } from "./retries.js"; import { Logger } from "./logger.js"; import { encodeForm } from "./encodings.js"; import { stringToBase64 } from "./base64.js"; import { SDKOptions, SDK_METADATA, serverURLFromOptions } from "./config.js"; import { SDKHooks } from "../hooks/hooks.js"; import { HookContext } from "../hooks/types.js"; import { ConnectionError, InvalidRequestError, RequestAbortedError, RequestTimeoutError, UnexpectedClientError, } from "../sdk/models/errors/httpclienterrors.js"; import { ERR, OK, Result } from "../sdk/types/fp.js"; export type RequestOptions = { /** * Sets a timeout, in milliseconds, on HTTP requests made by an SDK method. If * `fetchOptions.signal` is set then it will take precedence over this option. */ timeoutMs?: number; /** * Set or override a retry policy on HTTP calls. */ retries?: RetryConfig; /** * Specifies the status codes which should be retried using the given retry policy. */ retryCodes?: string[]; /** * Sets various request options on the `fetch` call made by an SDK method. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#options|Request} */ fetchOptions?: Omit; }; type RequestConfig = { method: string; path: string; baseURL?: string | URL; query?: string; body?: RequestInit["body"]; headers?: HeadersInit; security?: SecurityState | null; uaHeader?: string; timeoutMs?: number; }; const gt: unknown = typeof globalThis === "undefined" ? null : globalThis; const webWorkerLike = typeof gt === "object" && gt != null && "importScripts" in gt && typeof gt["importScripts"] === "function"; const isBrowserLike = webWorkerLike || (typeof navigator !== "undefined" && "serviceWorker" in navigator) || (typeof window === "object" && typeof window.document !== "undefined"); export class ClientSDK { private readonly httpClient: HTTPClient; protected readonly baseURL: URL | null; protected readonly hooks$: SDKHooks; protected readonly logger?: Logger | undefined; public readonly options$: SDKOptions & { hooks?: SDKHooks }; constructor(options: SDKOptions = {}) { const opt = options as unknown; if ( typeof opt === "object" && opt != null && "hooks" in opt && opt.hooks instanceof SDKHooks ) { this.hooks$ = opt.hooks; } else { this.hooks$ = new SDKHooks(); } this.options$ = { ...options, hooks: this.hooks$ }; const url = serverURLFromOptions(options); if (url) { url.pathname = url.pathname.replace(/\/+$/, "") + "/"; } const { baseURL, client } = this.hooks$.sdkInit({ baseURL: url, client: options.httpClient || new HTTPClient(), }); this.baseURL = baseURL; this.httpClient = client; this.logger = options.debugLogger; } public createRequest$( context: HookContext, conf: RequestConfig, options?: RequestOptions ): Result { const { method, path, query, headers: opHeaders, security } = conf; const base = conf.baseURL ?? this.baseURL; if (!base) { return ERR(new InvalidRequestError("No base URL provided for operation")); } const reqURL = new URL(base); const inputURL = new URL(path, reqURL); if (path) { reqURL.pathname += inputURL.pathname.replace(/^\/+/, ""); } let finalQuery = query || ""; const secQuery: string[] = []; for (const [k, v] of Object.entries(security?.queryParams || {})) { secQuery.push(encodeForm(k, v, { charEncoding: "percent" })); } if (secQuery.length) { finalQuery += `&${secQuery.join("&")}`; } if (finalQuery) { const q = finalQuery.startsWith("&") ? finalQuery.slice(1) : finalQuery; reqURL.search = `?${q}`; } const headers = new Headers(opHeaders); const username = security?.basic.username; const password = security?.basic.password; if (username != null || password != null) { const encoded = stringToBase64([username || "", password || ""].join(":")); headers.set("Authorization", `Basic ${encoded}`); } const securityHeaders = new Headers(security?.headers || {}); for (const [k, v] of securityHeaders) { headers.set(k, v); } let cookie = headers.get("cookie") || ""; for (const [k, v] of Object.entries(security?.cookies || {})) { cookie += `; ${k}=${v}`; } cookie = cookie.startsWith("; ") ? cookie.slice(2) : cookie; headers.set("cookie", cookie); const userHeaders = new Headers(options?.fetchOptions?.headers); for (const [k, v] of userHeaders) { headers.set(k, v); } // Only set user agent header in non-browser-like environments since CORS // policy disallows setting it in browsers e.g. Chrome throws an error. if (!isBrowserLike) { headers.set(conf.uaHeader ?? "user-agent", SDK_METADATA.userAgent); } let fetchOptions = options?.fetchOptions; if (!fetchOptions?.signal && conf.timeoutMs && conf.timeoutMs > 0) { const timeoutSignal = AbortSignal.timeout(conf.timeoutMs); if (!fetchOptions) { fetchOptions = { signal: timeoutSignal }; } else { fetchOptions.signal = timeoutSignal; } } let input; try { input = this.hooks$.beforeCreateRequest(context, { url: reqURL, options: { ...fetchOptions, body: conf.body ?? null, headers, method, }, }); } catch (err: unknown) { return ERR( new UnexpectedClientError("Create request hook failed to execute", { cause: err }) ); } return OK(new Request(input.url, input.options)); } public async do$( request: Request, options: { context: HookContext; errorCodes: number | string | (number | string)[]; retryConfig?: RetryConfig | undefined; retryCodes?: string[] | undefined; } ): Promise< Result< Response, RequestAbortedError | RequestTimeoutError | ConnectionError | UnexpectedClientError > > { const { context, errorCodes } = options; const retryConfig = options.retryConfig || { strategy: "none" }; const retryCodes = options.retryCodes || []; return retry( async () => { const req = await this.hooks$.beforeRequest(context, request.clone()); await logRequest(this.logger, req).catch((e) => this.logger?.log("Failed to log request:", e) ); let response = await this.httpClient.request(req); if (matchStatusCode(response, errorCodes)) { const result = await this.hooks$.afterError(context, response, null); if (result.error) { throw result.error; } response = result.response || response; } else { response = await this.hooks$.afterSuccess(context, response); } await logResponse(this.logger, response, req).catch((e) => this.logger?.log("Failed to log response:", e) ); return response; }, { config: retryConfig, statusCodes: retryCodes } ).then( (r) => OK(r), (err) => { switch (true) { case isAbortError(err): return ERR( new RequestAbortedError("Request aborted by client", { cause: err }) ); case isTimeoutError(err): return ERR(new RequestTimeoutError("Request timed out", { cause: err })); case isConnectionError(err): return ERR(new ConnectionError("Unable to make request", { cause: err })); default: return ERR( new UnexpectedClientError("Unexpected HTTP client error", { cause: err, }) ); } } ); } } const jsonLikeContentTypeRE = /^application\/(?:.{0,100}\+)?json/; async function logRequest(logger: Logger | undefined, req: Request) { if (!logger) { return; } const contentType = req.headers.get("content-type"); const ct = contentType?.split(";")[0] || ""; logger.group(`> Request: ${req.method} ${req.url}`); logger.group("Headers:"); for (const [k, v] of req.headers.entries()) { logger.log(`${k}: ${v}`); } logger.groupEnd(); logger.group("Body:"); switch (true) { case jsonLikeContentTypeRE.test(ct): logger.log(await req.clone().json()); break; case ct.startsWith("text/"): logger.log(await req.clone().text()); break; case ct === "multipart/form-data": { const body = await req.clone().formData(); for (const [k, v] of body) { const vlabel = v instanceof Blob ? "" : v; logger.log(`${k}: ${vlabel}`); } break; } default: logger.log(`<${contentType}>`); break; } logger.groupEnd(); logger.groupEnd(); } async function logResponse(logger: Logger | undefined, res: Response, req: Request) { if (!logger) { return; } const contentType = res.headers.get("content-type"); const ct = contentType?.split(";")[0] || ""; logger.group(`< Response: ${req.method} ${req.url}`); logger.log("Status Code:", res.status, res.statusText); logger.group("Headers:"); for (const [k, v] of res.headers.entries()) { logger.log(`${k}: ${v}`); } logger.groupEnd(); logger.group("Body:"); switch (true) { case matchContentType(res, "application/json") || jsonLikeContentTypeRE.test(ct): logger.log(await res.clone().json()); break; case matchContentType(res, "text/event-stream"): logger.log(`<${contentType}>`); break; case matchContentType(res, "text/*"): logger.log(await res.clone().text()); break; case matchContentType(res, "multipart/form-data"): { const body = await res.clone().formData(); for (const [k, v] of body) { const vlabel = v instanceof Blob ? "" : v; logger.log(`${k}: ${vlabel}`); } break; } default: logger.log(`<${contentType}>`); break; } logger.groupEnd(); logger.groupEnd(); }