UNPKG

@zayne-labs/callapi

Version:

A lightweight wrapper over fetch with quality of life improvements like built-in request cancellation, retries, interceptors and more

301 lines (294 loc) 10.1 kB
//#region src/types/type-helpers.ts const defineEnum = (value) => value; //#endregion //#region src/constants/default-options.ts const extraOptionDefaults = () => { return defineEnum({ bodySerializer: JSON.stringify, defaultHTTPErrorMessage: "An unexpected error occurred during the HTTP request.", dedupeCacheScope: "local", dedupeCacheScopeKey: "default", dedupeStrategy: "cancel", hooksExecutionMode: "parallel", hooksRegistrationOrder: "pluginsFirst", responseParser: JSON.parse, responseType: "json", resultMode: "all", retryAttempts: 0, retryCondition: () => true, retryDelay: 1e3, retryMaxDelay: 1e4, retryMethods: ["GET", "POST"], retryStatusCodes: [], retryStrategy: "linear" }); }; const requestOptionDefaults = () => { return defineEnum({ method: "GET" }); }; //#endregion //#region src/error.ts const httpErrorSymbol = Symbol("HTTPError"); var HTTPError = class HTTPError extends Error { errorData; httpErrorSymbol = httpErrorSymbol; isHTTPError = true; name = "HTTPError"; response; constructor(errorDetails, errorOptions) { const { defaultHTTPErrorMessage, errorData, response } = errorDetails; const resolvedDefaultHTTPErrorMessage = isString(defaultHTTPErrorMessage) ? defaultHTTPErrorMessage : defaultHTTPErrorMessage?.({ errorData, response }); const selectedDefaultErrorMessage = resolvedDefaultHTTPErrorMessage ?? (response.statusText || extraOptionDefaults().defaultHTTPErrorMessage); const message = errorData?.message ?? selectedDefaultErrorMessage; super(message, errorOptions); this.errorData = errorData; this.response = response; } /** * @description Checks if the given error is an instance of HTTPError * @param error - The error to check * @returns true if the error is an instance of HTTPError, false otherwise */ static isError(error) { if (!isObject(error)) return false; if (error instanceof HTTPError) return true; return error.httpErrorSymbol === httpErrorSymbol && error.isHTTPError === true; } }; const prettifyPath = (path) => { if (!path || path.length === 0) return ""; const pathString = path.map((segment) => isObject(segment) ? segment.key : segment).join("."); return ` → at ${pathString}`; }; const prettifyValidationIssues = (issues) => { const issuesString = issues.map((issue) => `✖ ${issue.message}${prettifyPath(issue.path)}`).join(" | "); return issuesString; }; const validationErrorSymbol = Symbol("validationErrorSymbol"); var ValidationError = class ValidationError extends Error { errorData; name = "ValidationError"; response; validationErrorSymbol = validationErrorSymbol; constructor(details, errorOptions) { const { issues, response } = details; const message = prettifyValidationIssues(issues); super(message, errorOptions); this.errorData = issues; this.response = response; } /** * @description Checks if the given error is an instance of HTTPError * @param error - The error to check * @returns true if the error is an instance of HTTPError, false otherwise */ static isError(error) { if (!isObject(error)) return false; if (error instanceof ValidationError) return true; return error.validationErrorSymbol === validationErrorSymbol && error.name === "ValidationError"; } }; //#endregion //#region src/utils/guards.ts const isHTTPError = (error) => { return isObject(error) && error.name === "HTTPError"; }; const isHTTPErrorInstance = (error) => { return HTTPError.isError(error); }; const isValidationError = (error) => { return isObject(error) && error.name === "ValidationError"; }; const isValidationErrorInstance = (error) => { return ValidationError.isError(error); }; const isJavascriptError = (error) => { return isObject(error) && !isHTTPError(error) && !isValidationError(error); }; const isArray = (value) => Array.isArray(value); const isObject = (value) => { return typeof value === "object" && value !== null; }; const hasObjectPrototype = (value) => { return Object.prototype.toString.call(value) === "[object Object]"; }; /** * @description Copied from TanStack Query's isPlainObject * @see https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts#L321 */ const isPlainObject = (value) => { if (!hasObjectPrototype(value)) return false; const constructor = value?.constructor; if (constructor === void 0) return true; const prototype = constructor.prototype; if (!hasObjectPrototype(prototype)) return false; if (!Object.hasOwn(prototype, "isPrototypeOf")) return false; if (Object.getPrototypeOf(value) !== Object.prototype) return false; return true; }; const isValidJsonString = (value) => { if (!isString(value)) return false; try { JSON.parse(value); return true; } catch { return false; } }; const isSerializable = (value) => { return isPlainObject(value) || isArray(value) || typeof value?.toJSON === "function"; }; const isFunction = (value) => typeof value === "function"; const isQueryString = (value) => isString(value) && value.includes("="); const isString = (value) => typeof value === "string"; const isReadableStream = (value) => { return value instanceof ReadableStream; }; //#endregion //#region src/auth.ts const getValue = (value) => { return isFunction(value) ? value() : value; }; const getAuthHeader = async (auth) => { if (auth === void 0) return; if (isString(auth) || auth === null) return { Authorization: `Bearer ${auth}` }; switch (auth.type) { case "Basic": { const username = await getValue(auth.username); const password = await getValue(auth.password); if (username === void 0 || password === void 0) return; return { Authorization: `Basic ${globalThis.btoa(`${username}:${password}`)}` }; } case "Custom": { const value = await getValue(auth.value); if (value === void 0) return; const prefix = await getValue(auth.prefix); return { Authorization: `${prefix} ${value}` }; } default: { const bearer = await getValue(auth.bearer); const token = await getValue(auth.token); if ("token" in auth && token !== void 0) return { Authorization: `Token ${token}` }; if (bearer === void 0) return; return { Authorization: `Bearer ${bearer}` }; } } }; //#endregion //#region src/constants/common.ts const fetchSpecificKeys = defineEnum([ "body", "integrity", "duplex", "method", "headers", "signal", "cache", "redirect", "window", "credentials", "keepalive", "referrer", "priority", "mode", "referrerPolicy" ]); //#endregion //#region src/utils/common.ts const omitKeys = (initialObject, keysToOmit) => { const updatedObject = {}; const keysToOmitSet = new Set(keysToOmit); for (const [key, value] of Object.entries(initialObject)) if (!keysToOmitSet.has(key)) updatedObject[key] = value; return updatedObject; }; const pickKeys = (initialObject, keysToPick) => { const updatedObject = {}; const keysToPickSet = new Set(keysToPick); for (const [key, value] of Object.entries(initialObject)) if (keysToPickSet.has(key)) updatedObject[key] = value; return updatedObject; }; const splitBaseConfig = (baseConfig) => [pickKeys(baseConfig, fetchSpecificKeys), omitKeys(baseConfig, fetchSpecificKeys)]; const splitConfig = (config) => [pickKeys(config, fetchSpecificKeys), omitKeys(config, fetchSpecificKeys)]; const toQueryString = (params) => { if (!params) { console.error("toQueryString:", "No query params provided!"); return null; } return new URLSearchParams(params).toString(); }; const objectifyHeaders = (headers) => { if (!headers || isPlainObject(headers)) return headers; return Object.fromEntries(headers); }; const getHeaders = async (options) => { const { auth, body, headers } = options; const shouldResolveHeaders = Boolean(headers) || Boolean(body) || Boolean(auth); if (!shouldResolveHeaders) return; const headersObject = { ...await getAuthHeader(auth), ...objectifyHeaders(headers) }; if (isQueryString(body)) { headersObject["Content-Type"] = "application/x-www-form-urlencoded"; return headersObject; } if (isSerializable(body) || isValidJsonString(body)) { headersObject["Content-Type"] = "application/json"; headersObject.Accept = "application/json"; } return headersObject; }; const getBody = (options) => { const { body, bodySerializer } = options; if (isSerializable(body)) { const selectedBodySerializer = bodySerializer ?? extraOptionDefaults().bodySerializer; return selectedBodySerializer(body); } return body; }; const getFetchImpl = (customFetchImpl) => { if (customFetchImpl) return customFetchImpl; if (typeof globalThis !== "undefined" && isFunction(globalThis.fetch)) return globalThis.fetch; throw new Error("No fetch implementation found"); }; const PromiseWithResolvers = () => { let reject; let resolve; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, reject, resolve }; }; const waitFor = (delay) => { if (delay === 0) return; const { promise, resolve } = PromiseWithResolvers(); setTimeout(resolve, delay); return promise; }; const createCombinedSignal = (...signals) => { const cleanedSignals = signals.filter(Boolean); const combinedSignal = AbortSignal.any(cleanedSignals); return combinedSignal; }; const createTimeoutSignal = (milliseconds) => AbortSignal.timeout(milliseconds); const deterministicHashFn = (value) => { return JSON.stringify(value, (_, val) => { if (!isPlainObject(val)) return val; const sortedKeys = Object.keys(val).sort(); const result = {}; for (const key of sortedKeys) result[key] = val[key]; return result; }); }; const toArray = (value) => isArray(value) ? value : [value]; //#endregion export { HTTPError, ValidationError, createCombinedSignal, createTimeoutSignal, defineEnum, deterministicHashFn, extraOptionDefaults, getBody, getFetchImpl, getHeaders, isArray, isFunction, isHTTPError, isHTTPErrorInstance, isJavascriptError, isObject, isPlainObject, isReadableStream, isString, isValidationError, isValidationErrorInstance, requestOptionDefaults, splitBaseConfig, splitConfig, toArray, toQueryString, waitFor }; //# sourceMappingURL=utils-iI2CAZIV.js.map