@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
JavaScript
//#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