next-unified-query-core
Version:
Framework-agnostic HTTP client with built-in query state management
1,671 lines (1,654 loc) • 88.3 kB
JavaScript
'use strict';
var compat = require('es-toolkit/compat');
var esToolkit = require('es-toolkit');
var v4 = require('zod/v4');
var predicate = require('es-toolkit/predicate');
var QuickLRU = require('quick-lru');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var QuickLRU__default = /*#__PURE__*/_interopDefault(QuickLRU);
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/types/index.ts
exports.ContentType = void 0; exports.ResponseType = void 0; exports.FetchError = void 0;
var init_types = __esm({
"src/types/index.ts"() {
exports.ContentType = /* @__PURE__ */ ((ContentType2) => {
ContentType2["JSON"] = "application/json";
ContentType2["FORM"] = "application/x-www-form-urlencoded";
ContentType2["TEXT"] = "text/plain";
ContentType2["BLOB"] = "application/octet-stream";
ContentType2["MULTIPART"] = "multipart/form-data";
ContentType2["XML"] = "application/xml";
ContentType2["HTML"] = "text/html";
return ContentType2;
})(exports.ContentType || {});
exports.ResponseType = /* @__PURE__ */ ((ResponseType2) => {
ResponseType2["JSON"] = "json";
ResponseType2["TEXT"] = "text";
ResponseType2["BLOB"] = "blob";
ResponseType2["ARRAY_BUFFER"] = "arraybuffer";
ResponseType2["RAW"] = "raw";
return ResponseType2;
})(exports.ResponseType || {});
exports.FetchError = class extends Error {
/**
* FetchError 생성자
* @param message 에러 메시지
* @param config 요청 설정
* @param code 에러 코드
* @param request 요청 객체
* @param response 응답 객체
* @param responseData 응답 데이터
*/
constructor(message, config, code, request2, response, responseData) {
super(message);
/**
* 에러 이름
*/
this.name = "FetchError";
this.config = config;
this.code = code;
this.request = request2;
if (response) {
this.response = {
data: responseData,
status: response.status,
statusText: response.statusText,
headers: response.headers
};
}
}
};
}
});
// src/interceptors.ts
function createInterceptors() {
const requestInterceptors = new RequestInterceptorManager();
const responseInterceptors = new ResponseInterceptorManager();
const errorInterceptors = new ErrorInterceptorManager();
return {
request: {
/**
* 요청 인터셉터 추가 - 자동으로 같은 유형의 이전 인터셉터 교체
* @param interceptor 요청 처리 함수
* @returns 제거 함수가 포함된 핸들
* @example
* ```typescript
* // 인터셉터 추가 (auth 유형 기본값)
* const authInterceptor = api.interceptors.request.use(config => {
* config.headers = config.headers || {};
* config.headers['Authorization'] = `Bearer ${getToken()}`;
* return config;
* });
*
* // 다른 인터셉터 추가 시 이전 auth 유형 인터셉터는 자동 제거됨
* api.interceptors.request.use(config => {
* config.headers = config.headers || {};
* config.headers['Authorization'] = `Bearer ${getNewToken()}`;
* return config;
* });
* ```
*/
use: (interceptor) => requestInterceptors.use(interceptor),
/**
* 인터셉터 제거 (하위 호환성)
* @deprecated InterceptorHandle.remove() 사용 권장
*/
eject: (id) => {
console.warn("eject() \uBA54\uC11C\uB4DC\uB294 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. use()\uAC00 \uBC18\uD658\uD558\uB294 InterceptorHandle.remove()\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.");
},
/**
* 모든 요청 인터셉터 제거
* @example
* ```typescript
* // 모든 요청 인터셉터 제거
* api.interceptors.request.clear();
* ```
*/
clear: () => requestInterceptors.clear(),
/**
* 특정 유형의 인터셉터 제거
* @param type 제거할 인터셉터 유형
* @example
* ```typescript
* // 인증 관련 인터셉터만 제거
* api.interceptors.request.clearByType(interceptorTypes.auth);
* ```
*/
clearByType: (type) => requestInterceptors.clearByType(type),
/**
* 요청 인터셉터 실행 (내부용)
*/
run: requestInterceptors.run.bind(requestInterceptors),
/**
* 디버깅 용도로 현재 등록된 인터셉터 정보 조회
*/
getRegistered: () => requestInterceptors.getRegisteredInterceptors()
},
response: {
/**
* 응답 인터셉터 추가 - 자동으로 같은 유형의 이전 인터셉터 교체
* @param onFulfilled 성공 응답 처리 함수
* @returns 제거 함수가 포함된 핸들
*/
use: (onFulfilled) => responseInterceptors.use(onFulfilled),
/**
* 인터셉터 제거 (하위 호환성)
* @deprecated InterceptorHandle.remove() 사용 권장
*/
eject: (id) => {
console.warn("eject() \uBA54\uC11C\uB4DC\uB294 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. use()\uAC00 \uBC18\uD658\uD558\uB294 InterceptorHandle.remove()\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.");
},
/**
* 모든 응답 인터셉터 제거
*/
clear: () => responseInterceptors.clear(),
/**
* 특정 유형의 인터셉터 제거
* @param type 제거할 인터셉터 유형
*/
clearByType: (type) => responseInterceptors.clearByType(type),
/**
* 응답 인터셉터 실행 (내부용)
*/
run: responseInterceptors.run.bind(responseInterceptors),
/**
* 디버깅 용도로 현재 등록된 인터셉터 정보 조회
*/
getRegistered: () => responseInterceptors.getRegisteredInterceptors()
},
error: {
/**
* 에러 인터셉터 추가 - 자동으로 같은 유형의 이전 인터셉터 교체
* @param onRejected 에러 처리 함수
* @returns 제거 함수가 포함된 핸들
*/
use: (onRejected) => errorInterceptors.use(onRejected),
/**
* 인터셉터 제거 (하위 호환성)
* @deprecated InterceptorHandle.remove() 사용 권장
*/
eject: (id) => {
console.warn("eject() \uBA54\uC11C\uB4DC\uB294 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. use()\uAC00 \uBC18\uD658\uD558\uB294 InterceptorHandle.remove()\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.");
},
/**
* 모든 에러 인터셉터 제거
*/
clear: () => errorInterceptors.clear(),
/**
* 특정 유형의 인터셉터 제거
* @param type 제거할 인터셉터 유형
*/
clearByType: (type) => errorInterceptors.clearByType(type),
/**
* 에러 인터셉터 실행 (내부용)
*/
run: errorInterceptors.run.bind(errorInterceptors),
/**
* 디버깅 용도로 현재 등록된 인터셉터 정보 조회
*/
getRegistered: () => errorInterceptors.getRegisteredInterceptors()
}
};
}
var interceptorTypeSymbols, InterceptorManager, RequestInterceptorManager, ResponseInterceptorManager, ErrorInterceptorManager; exports.interceptorTypes = void 0;
var init_interceptors = __esm({
"src/interceptors.ts"() {
interceptorTypeSymbols = {
// 기본 인터셉터 유형
default: Symbol("default-interceptor"),
// 인증 관련 인터셉터 - 자동 교체 설정 가능
auth: Symbol("auth-interceptor"),
// 로깅 관련 인터셉터 - 자동 교체 설정 가능
logging: Symbol("logging-interceptor"),
// 에러 처리 인터셉터 - 자동 교체 설정 가능
errorHandler: Symbol("error-handler-interceptor")
};
InterceptorManager = class {
constructor() {
this.handlers = [];
this.idCounter = 0;
}
/**
* 인터셉터 추가
* @param handler 인터셉터 핸들러 함수
* @param options 인터셉터 등록 옵션
* @returns 제거 함수가 포함된 핸들 객체
* @note 여러 번 use를 호출하면 등록한 모든 인터셉터가 순차적으로 실행됩니다. (Axios 등과 동일)
*/
use(handler, options2) {
const id = this.idCounter++;
const type = options2?.type || interceptorTypeSymbols.default;
const tag = options2?.tag || "unnamed-interceptor";
this.handlers.push({
id,
handler,
type,
tag
});
return {
remove: () => this.eject(id)
};
}
/**
* 인터셉터 제거
* @param id 제거할 인터셉터 ID
*/
eject(id) {
const index = this.handlers.findIndex((h) => h !== null && h.id === id);
if (index !== -1) {
this.handlers[index] = null;
}
}
/**
* 특정 유형의 인터셉터 모두 제거
* @param type 제거할 인터셉터 유형
*/
ejectByType(type) {
this.handlers.forEach((item, index) => {
if (item !== null && item.type === type) {
this.handlers[index] = null;
}
});
}
/**
* 모든 인터셉터 제거
*/
clear() {
this.handlers = [];
}
/**
* 모든 인터셉터 실행
* @param value 인터셉터에 전달할 값
* @returns 처리된 값
*/
async forEach(value) {
let result = value;
for (const handler of this.handlers) {
if (handler !== null) {
result = await handler.handler(result);
}
}
return result;
}
/**
* 디버깅 용도로 현재 등록된 인터셉터 정보 반환
*/
getRegisteredInterceptors() {
return this.handlers.filter((h) => h !== null).map((h) => ({
id: h?.id,
tag: h?.tag || "(unnamed)",
type: h?.type?.description || "default"
}));
}
};
RequestInterceptorManager = class {
constructor() {
this.manager = new InterceptorManager();
}
/**
* 요청 인터셉터 추가
* @param interceptor 요청 인터셉터 함수
* @param type 인터셉터 유형 - 기본값은 auth 타입 (자동 교체됨)
* @returns 제거 함수가 포함된 핸들 객체
*/
use(interceptor, type = interceptorTypeSymbols.auth) {
return this.manager.use(interceptor, { type, tag: "request-interceptor" });
}
/**
* 모든 요청 인터셉터 제거
*/
clear() {
this.manager.clear();
}
/**
* 특정 유형의 인터셉터 모두 제거
* @param type 제거할 인터셉터 유형
*/
clearByType(type) {
this.manager.ejectByType(type);
}
/**
* 요청 인터셉터 실행
*/
async run(config) {
return this.manager.forEach(config);
}
/**
* 디버깅 용도로 현재 등록된 인터셉터 정보 반환
*/
getRegisteredInterceptors() {
return this.manager.getRegisteredInterceptors();
}
};
ResponseInterceptorManager = class {
constructor() {
this.manager = new InterceptorManager();
}
/**
* 응답 인터셉터 추가
* @param interceptor 응답 인터셉터 함수
* @param type 인터셉터 유형 - 기본값은 auth 타입 (자동 교체됨)
* @returns 제거 함수가 포함된 핸들 객체
*/
use(interceptor, type = interceptorTypeSymbols.auth) {
return this.manager.use(interceptor, { type, tag: "response-interceptor" });
}
/**
* 모든 응답 인터셉터 제거
*/
clear() {
this.manager.clear();
}
/**
* 특정 유형의 인터셉터 제거
* @param type 제거할 인터셉터 유형
*/
clearByType(type) {
this.manager.ejectByType(type);
}
/**
* 응답 인터셉터 실행 (내부용)
*/
async run(response) {
return this.manager.forEach(response);
}
/**
* 디버깅 용도로 현재 등록된 인터셉터 정보 반환
*/
getRegisteredInterceptors() {
return this.manager.getRegisteredInterceptors();
}
};
ErrorInterceptorManager = class {
constructor() {
this.manager = new InterceptorManager();
}
/**
* 에러 인터셉터 추가
* @param interceptor 에러 인터셉터 함수
* @param type 인터셉터 유형 - 기본값은 errorHandler 타입 (자동 교체됨)
* @returns 제거 함수가 포함된 핸들 객체
*/
use(interceptor, type = interceptorTypeSymbols.errorHandler) {
return this.manager.use(interceptor, { type, tag: "error-interceptor" });
}
/**
* 모든 에러 인터셉터 제거
*/
clear() {
this.manager.clear();
}
/**
* 특정 유형의 인터셉터 제거
* @param type 제거할 인터셉터 유형
*/
clearByType(type) {
this.manager.ejectByType(type);
}
/**
* 에러 인터셉터 실행 (내부용)
*/
async run(error) {
return this.manager.forEach(error);
}
/**
* 디버깅 용도로 현재 등록된 인터셉터 정보 반환
*/
getRegisteredInterceptors() {
return this.manager.getRegisteredInterceptors();
}
};
exports.interceptorTypes = interceptorTypeSymbols;
}
});
function appendQueryParams(url, params) {
const cleanUrl = esToolkit.trim(url);
if (!params || compat.isEmpty(params)) return cleanUrl;
const validParams = esToolkit.pickBy(params, (value) => !esToolkit.isNil(value));
if (compat.isEmpty(validParams)) return cleanUrl;
const [baseUrl, fragment] = cleanUrl.split("#");
const [path, existingQuery] = baseUrl.split("?");
const existingParams = new URLSearchParams(existingQuery || "");
Object.entries(validParams).forEach(([key, value]) => {
existingParams.set(key, String(value));
});
const queryString = existingParams.toString();
const urlParts = esToolkit.compact([path, queryString ? `?${queryString}` : null, fragment ? `#${fragment}` : null]);
return urlParts.join("");
}
function combineURLs(baseURL, url) {
const cleanBaseURL = baseURL ? esToolkit.trim(baseURL) : "";
const cleanUrl = url ? esToolkit.trim(url) : "";
if (!cleanBaseURL) return cleanUrl;
if (!cleanUrl) return cleanBaseURL;
const baseEndsWithSlash = cleanBaseURL.endsWith("/");
const urlStartsWithSlash = cleanUrl.startsWith("/");
if (baseEndsWithSlash && urlStartsWithSlash) {
return cleanBaseURL + cleanUrl.substring(1);
}
if (!baseEndsWithSlash && !urlStartsWithSlash) {
return `${cleanBaseURL}/${cleanUrl}`;
}
return cleanBaseURL + cleanUrl;
}
var init_url = __esm({
"src/utils/url.ts"() {
}
});
// src/utils/config.ts
function mergeConfigs(defaultConfig = {}, requestConfig = {}) {
const mergedConfig = {
...defaultConfig,
...requestConfig
};
mergedConfig.headers = {
...defaultConfig.headers,
...requestConfig.headers
};
mergedConfig.params = {
...defaultConfig.params,
...requestConfig.params
};
if (defaultConfig.next || requestConfig.next) {
mergedConfig.next = {
...defaultConfig.next,
...requestConfig.next
};
}
return mergedConfig;
}
var init_config = __esm({
"src/utils/config.ts"() {
}
});
function stringifyData(data) {
if (esToolkit.isNil(data)) return null;
if (esToolkit.isString(data)) return data;
try {
return JSON.stringify(data);
} catch (e) {
console.error("Failed to stringify data:", e);
return null;
}
}
var init_data = __esm({
"src/utils/data.ts"() {
}
});
// src/utils/timeout.ts
function createTimeoutPromise(ms) {
if (!ms || ms <= 0) return null;
const controller = new AbortController();
const promise = new Promise((_, reject) => {
setTimeout(() => {
controller.abort();
reject(new Error(`Request timeout of ${ms}ms exceeded`));
}, ms);
});
return { promise, controller };
}
var init_timeout = __esm({
"src/utils/timeout.ts"() {
}
});
// src/utils/response.ts
function unwrap(response) {
return response.data;
}
function getStatus(response) {
return response.status;
}
function getHeaders(response) {
return response.headers;
}
function hasStatus(response, code) {
return response.status === code;
}
function createError(message, config, code = "ERR_UNKNOWN", response, data) {
return new exports.FetchError(message, config, code, void 0, response, data);
}
var init_response = __esm({
"src/utils/response.ts"() {
init_types();
}
});
function isFetchError(error) {
return error instanceof exports.FetchError;
}
function isValidationError(error) {
return isFetchError(error) && error.code === "ERR_VALIDATION" && error.cause instanceof v4.z.ZodError;
}
function getValidationErrors(error) {
if (!isValidationError(error)) {
return [];
}
return error.cause.issues.map((issue) => ({
path: issue.path.join("."),
message: issue.message
}));
}
function hasErrorCode(error, code) {
return isFetchError(error) && error.code === code;
}
function handleFetchError(error, handlers) {
if (isFetchError(error) && error.code) {
const errorCode = error.code;
const handler = handlers[errorCode];
if (handler) {
return handler(error);
}
}
if (handlers.default) {
return handlers.default(error);
}
throw error;
}
function handleHttpError(error, handlers) {
if (isFetchError(error) && error.response && esToolkit.isFunction(handlers[error.response.status])) {
return handlers[error.response.status](error);
}
if (handlers.default) {
return handlers.default(error);
}
throw error;
}
function errorToResponse(error, data) {
return {
data,
status: error.response?.status || 500,
statusText: error.response?.statusText || error.message,
headers: error.response?.headers || new Headers(),
config: error.config,
request: error.request
};
}
exports.ErrorCode = void 0;
var init_error = __esm({
"src/utils/error.ts"() {
init_types();
exports.ErrorCode = {
/** 네트워크 에러 */
NETWORK: "ERR_NETWORK",
/** 요청 취소됨 */
CANCELED: "ERR_CANCELED",
/** 요청 타임아웃 */
TIMEOUT: "ERR_TIMEOUT",
/** 서버 응답 에러 (4xx, 5xx) */
BAD_RESPONSE: "ERR_BAD_RESPONSE",
/** 데이터 검증 실패 */
VALIDATION: "ERR_VALIDATION",
/** 알 수 없는 검증 오류 */
VALIDATION_UNKNOWN: "ERR_VALIDATION_UNKNOWN",
/** 알 수 없는 에러 */
UNKNOWN: "ERR_UNKNOWN"
};
}
});
// src/utils/index.ts
var init_utils = __esm({
"src/utils/index.ts"() {
init_url();
init_config();
init_data();
init_timeout();
init_response();
init_error();
}
});
function isJsonContentType(contentType) {
return contentType === "application/json" /* JSON */ || contentType.includes("application/json");
}
function isFormContentType(contentType) {
return contentType === "application/x-www-form-urlencoded" /* FORM */ || contentType.includes("application/x-www-form-urlencoded");
}
function isXmlContentType(contentType) {
return contentType === "application/xml" /* XML */ || contentType.includes("application/xml");
}
function isHtmlContentType(contentType) {
return contentType === "text/html" /* HTML */ || contentType.includes("text/html");
}
function isTextContentType(contentType) {
return contentType === "text/plain" /* TEXT */ || contentType.includes("text/plain");
}
function isBlobContentType(contentType) {
return contentType === "application/octet-stream" /* BLOB */ || contentType.includes("application/octet-stream");
}
function createFormBody(data) {
if (compat.isObject(data) && !(data instanceof URLSearchParams)) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
if (!predicate.isNil(value)) {
params.append(key, String(value));
}
}
return params;
}
if (data instanceof URLSearchParams) {
return data;
}
return String(data || "");
}
function createTextBody(data) {
return compat.isString(data) ? data : String(data);
}
function createBlobBody(data) {
if (data instanceof Blob || data instanceof ArrayBuffer) {
return data;
}
return compat.isString(data) ? data : String(data);
}
function isPlainObjectForJson(data) {
return compat.isObject(data) && !(data instanceof FormData) && !(data instanceof URLSearchParams) && !(data instanceof Blob);
}
function shouldDefaultToJson(effectiveContentType, data) {
return effectiveContentType === "" && isPlainObjectForJson(data);
}
function createRetrySettings(retryConfig) {
const defaultBackoff = (count) => Math.min(1e3 * 2 ** (count - 1), 1e4);
if (compat.isNumber(retryConfig)) {
return {
maxRetries: retryConfig,
retryStatusCodes: [],
retryBackoff: defaultBackoff
};
}
if (retryConfig && compat.isObject(retryConfig)) {
let retryBackoff = defaultBackoff;
if (retryConfig.backoff === "linear") {
retryBackoff = (count) => 1e3 * count;
} else if (retryConfig.backoff === "exponential") {
retryBackoff = (count) => Math.min(1e3 * 2 ** (count - 1), 1e4);
} else if (compat.isFunction(retryConfig.backoff)) {
retryBackoff = retryConfig.backoff;
}
return {
maxRetries: retryConfig.limit,
retryStatusCodes: retryConfig.statusCodes || [],
retryBackoff
};
}
return {
maxRetries: 0,
retryStatusCodes: [],
retryBackoff: defaultBackoff
};
}
function setupAbortSignal(signal, onAbort) {
if (!signal) return;
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener("abort", onAbort);
}
}
function throwIfCanceled(isCanceled, config) {
if (isCanceled) {
throw new exports.FetchError("Request was canceled", config, "ERR_CANCELED");
}
}
function createRequestInit(requestConfig, abortController) {
const {
method = "GET",
headers = {},
cache,
credentials,
integrity,
keepalive,
mode,
redirect,
referrer,
referrerPolicy,
next
} = requestConfig;
const requestInit = {
method,
headers,
signal: abortController.signal,
cache,
credentials,
integrity,
keepalive,
mode,
redirect,
referrer,
referrerPolicy
};
if (next) {
requestInit.next = next;
}
return requestInit;
}
function prepareRequestBody(data, contentType, headers) {
const headersCopy = { ...headers };
if (data instanceof FormData || data instanceof URLSearchParams || data instanceof Blob) {
if (data instanceof FormData && (contentType === "" || contentType === "multipart/form-data" /* MULTIPART */)) {
const { "Content-Type": _, ...remainingHeaders } = headersCopy;
return { body: data, headers: remainingHeaders };
}
return { body: data, headers: headersCopy };
}
const contentTypeStr = String(contentType);
if (isJsonContentType(contentTypeStr)) {
return {
body: stringifyData(data),
headers: { ...headersCopy, "Content-Type": "application/json" /* JSON */ }
};
}
if (isFormContentType(contentTypeStr)) {
return {
body: createFormBody(data),
headers: { ...headersCopy, "Content-Type": "application/x-www-form-urlencoded" /* FORM */ }
};
}
if (isXmlContentType(contentTypeStr)) {
return {
body: createTextBody(data),
headers: { ...headersCopy, "Content-Type": "application/xml" /* XML */ }
};
}
if (isHtmlContentType(contentTypeStr)) {
return {
body: createTextBody(data),
headers: { ...headersCopy, "Content-Type": "text/html" /* HTML */ }
};
}
if (isTextContentType(contentTypeStr)) {
return {
body: createTextBody(data),
headers: { ...headersCopy, "Content-Type": "text/plain" /* TEXT */ }
};
}
if (isBlobContentType(contentTypeStr)) {
return {
body: createBlobBody(data),
headers: { ...headersCopy, "Content-Type": "application/octet-stream" /* BLOB */ }
};
}
const body = compat.isObject(data) ? stringifyData(data) : String(data);
return {
body,
headers: { ...headersCopy, "Content-Type": contentTypeStr }
};
}
async function processResponseByType(response, responseType, contentTypeHeader, parseJSON = true) {
const effectiveResponseType = responseType || (contentTypeHeader.includes("application/json") && parseJSON !== false ? "json" /* JSON */ : "text" /* TEXT */);
const isEmptyResponse = response.status === 204 || response.headers.get("content-length") === "0";
const safeCall = async (method, fallback) => {
if (!response[method] || !compat.isFunction(response[method])) {
if (typeof process !== "undefined" && process.env.NODE_ENV === "test") {
try {
if (response[method] && compat.isFunction(response[method])) {
return await response[method]();
}
} catch (e) {
}
}
return fallback;
}
try {
return await response[method]();
} catch (e) {
console.warn(`Failed to process response with ${method}:`, e);
return fallback;
}
};
switch (effectiveResponseType) {
case "json" /* JSON */:
if (isEmptyResponse) {
return {};
}
try {
return await response.json();
} catch (e) {
return await safeCall("text", "");
}
case "blob" /* BLOB */:
if (isEmptyResponse) {
return new Blob();
}
return await safeCall("blob", new Blob());
case "arraybuffer" /* ARRAY_BUFFER */:
if (isEmptyResponse) {
return new ArrayBuffer(0);
}
return await safeCall("arrayBuffer", new ArrayBuffer(0));
case "raw" /* RAW */:
return response;
default:
if (isEmptyResponse) {
return "";
}
return await safeCall("text", "");
}
}
function canRetry(retryCount, maxRetries, isCanceled) {
return retryCount < maxRetries && !isCanceled;
}
function shouldRetryForHttpStatus(status, retryStatusCodes) {
return retryStatusCodes.length === 0 || retryStatusCodes.includes(status);
}
function isNetworkError(error) {
return !(error instanceof exports.FetchError);
}
async function executeRetry(retryBackoff, retryCount, performRequest) {
const delay = retryBackoff(retryCount);
await new Promise((resolve) => setTimeout(resolve, delay));
return performRequest();
}
function hasValidAuthRetry(authRetryOption) {
return !predicate.isNil(authRetryOption) && compat.isFunction(authRetryOption.handler);
}
function shouldAuthRetryForStatus(status, authRetryOption) {
const statusCodes = authRetryOption.statusCodes ?? [401];
return statusCodes.includes(status);
}
function shouldExecuteAuthRetry(fetchError, config, authRetryOption) {
return !authRetryOption.shouldRetry || authRetryOption.shouldRetry(fetchError, config);
}
function canAuthRetry(authRetryCount, authRetryOption) {
return authRetryCount < (authRetryOption.limit ?? 1);
}
async function processErrorWithInterceptor(error, interceptors2) {
const processedError = await interceptors2.error.run(error);
if ("data" in processedError && "status" in processedError) {
return processedError;
}
throw processedError;
}
function createRequestFunction(defaultConfig, interceptors2) {
const activeRequests = /* @__PURE__ */ new Map();
function request2(config) {
const requestKey = JSON.stringify({
url: config.url,
method: config.method || "GET",
params: config.params,
data: config.data,
baseURL: config.baseURL,
_authRetryCount: config._authRetryCount || 0
});
const existingRequest = activeRequests.get(requestKey);
if (existingRequest) {
return existingRequest;
}
let isCanceled = false;
let abortController = new AbortController();
const cancel = () => {
isCanceled = true;
abortController.abort();
};
const { maxRetries, retryStatusCodes, retryBackoff } = createRetrySettings(config.retry);
let retryCount = 0;
let authRetryCount = config._authRetryCount || 0;
const authRetryOption = config.authRetry || defaultConfig.authRetry;
setupAbortSignal(config.signal, () => {
isCanceled = true;
abortController.abort();
});
async function performRequest() {
try {
throwIfCanceled(isCanceled, config);
const { schema, ...restConfig } = config;
const requestConfig = await interceptors2.request.run(restConfig);
const url = combineURLs(requestConfig.baseURL, requestConfig.url);
const fullUrl = appendQueryParams(url, requestConfig.params);
const timeoutResult = createTimeoutPromise(requestConfig.timeout);
throwIfCanceled(isCanceled, config);
const { contentType, responseType, data, headers = {} } = requestConfig;
const requestInit = createRequestInit(requestConfig, abortController);
if (!predicate.isNil(data)) {
const effectiveContentType = contentType || headers["Content-Type"] || "";
if (shouldDefaultToJson(effectiveContentType, data)) {
requestInit.body = stringifyData(data);
requestInit.headers = {
...headers,
"Content-Type": "application/json" /* JSON */
};
} else {
const { body, headers: processedHeaders } = prepareRequestBody(
data,
effectiveContentType,
headers
);
requestInit.body = body;
requestInit.headers = processedHeaders;
}
}
throwIfCanceled(isCanceled, config);
const fetchPromise = fetch(fullUrl, requestInit);
const response = await (timeoutResult ? Promise.race([fetchPromise, timeoutResult.promise]) : fetchPromise);
const contentTypeHeader = response.headers.get("content-type") || "";
const responseData = await processResponseByType(
response,
responseType,
contentTypeHeader,
requestConfig.parseJSON
);
if (!response.ok) {
const fetchError = new exports.FetchError(
response.statusText || `HTTP error ${response.status}`,
requestConfig,
"ERR_BAD_RESPONSE",
requestInit,
response,
responseData
);
if (hasValidAuthRetry(authRetryOption)) {
const statusMatch = shouldAuthRetryForStatus(response.status, authRetryOption);
const shouldRetryResult = shouldExecuteAuthRetry(fetchError, config, authRetryOption);
if (statusMatch && shouldRetryResult) {
authRetryCount = config._authRetryCount || 0;
if (canAuthRetry(authRetryCount, authRetryOption)) {
const shouldRetry = await authRetryOption.handler(fetchError, config);
if (shouldRetry) {
return request2({
...config,
_authRetryCount: authRetryCount + 1
});
}
}
}
}
if (!canRetry(retryCount, maxRetries, isCanceled)) {
return processErrorWithInterceptor(fetchError, interceptors2);
}
if (shouldRetryForHttpStatus(response.status, retryStatusCodes)) {
retryCount++;
abortController = new AbortController();
return executeRetry(retryBackoff, retryCount, performRequest);
}
return processErrorWithInterceptor(fetchError, interceptors2);
}
const NextTypeResponse = {
data: responseData,
status: response.status,
statusText: response.statusText,
headers: response.headers,
config: requestConfig,
request: requestInit
};
const processedResponse = await interceptors2.response.run(NextTypeResponse);
if (schema) {
try {
const validatedData = schema.parse(processedResponse.data);
processedResponse.data = validatedData;
return processedResponse;
} catch (validationError) {
if (validationError instanceof v4.z.ZodError) {
const fetchError2 = new exports.FetchError(
"Validation failed",
requestConfig,
"ERR_VALIDATION",
requestInit,
response,
processedResponse.data
);
fetchError2.name = "ValidationError";
fetchError2.cause = validationError;
return processErrorWithInterceptor(fetchError2, interceptors2);
}
const fetchError = new exports.FetchError(
"Unknown validation error",
requestConfig,
"ERR_VALIDATION_UNKNOWN",
requestInit,
response,
processedResponse.data
);
return processErrorWithInterceptor(fetchError, interceptors2);
}
}
return processedResponse;
} catch (error) {
if (error instanceof exports.FetchError) {
throw error;
}
if (error instanceof Error && error.name === "AbortError") {
const fetchError2 = new exports.FetchError(
isCanceled ? "Request was canceled" : "Request timed out",
config,
isCanceled ? "ERR_CANCELED" : "ERR_TIMEOUT"
);
return processErrorWithInterceptor(fetchError2, interceptors2);
}
if (canRetry(retryCount, maxRetries, isCanceled) && isNetworkError(error)) {
retryCount++;
abortController = new AbortController();
return executeRetry(retryBackoff, retryCount, performRequest);
}
const fetchError = new exports.FetchError(
error instanceof Error ? error.message : "Request failed",
config,
"ERR_NETWORK"
);
return processErrorWithInterceptor(fetchError, interceptors2);
}
}
const requestPromise = performRequest().finally(() => {
activeRequests.delete(requestKey);
});
activeRequests.set(requestKey, requestPromise);
const cancelablePromise = Object.assign(requestPromise, {
cancel,
isCanceled: () => isCanceled
});
return cancelablePromise;
}
return request2;
}
var init_request = __esm({
"src/core/request.ts"() {
init_types();
init_utils();
}
});
// src/methods/index.ts
function createHttpMethods(request2, defaultConfig) {
return {
get(url, config = {}) {
return request2(
mergeConfigs(defaultConfig, {
...config,
url,
method: "GET"
})
);
},
post(url, data, config = {}) {
return request2(
mergeConfigs(defaultConfig, {
...config,
url,
method: "POST",
data
})
);
},
put(url, data, config = {}) {
return request2(
mergeConfigs(defaultConfig, {
...config,
url,
method: "PUT",
data
})
);
},
delete(url, config = {}) {
return request2(
mergeConfigs(defaultConfig, {
...config,
url,
method: "DELETE"
})
);
},
patch(url, data, config = {}) {
return request2(
mergeConfigs(defaultConfig, {
...config,
url,
method: "PATCH",
data
})
);
},
head(url, config = {}) {
return request2(
mergeConfigs(defaultConfig, {
...config,
url,
method: "HEAD"
})
);
},
options(url, config = {}) {
return request2(
mergeConfigs(defaultConfig, {
...config,
url,
method: "OPTIONS"
})
);
}
};
}
var init_methods = __esm({
"src/methods/index.ts"() {
init_utils();
}
});
// src/core/client.ts
function createFetch(defaultConfig = {}) {
const mergedConfig = {
...defaultConfig
};
const interceptors2 = createInterceptors();
const request2 = createRequestFunction(mergedConfig, interceptors2);
const methods = createHttpMethods(request2, mergedConfig);
const instance = {
defaults: mergedConfig,
interceptors: interceptors2,
request: request2,
...methods
};
return instance;
}
var init_client = __esm({
"src/core/client.ts"() {
init_interceptors();
init_request();
init_methods();
init_error();
}
});
// src/fetch.ts
var fetch_exports = {};
__export(fetch_exports, {
default: () => exports.defaultInstance,
del: () => exports.del,
get: () => exports.get,
head: () => exports.head,
interceptors: () => exports.interceptors,
ntFetch: () => exports.ntFetch,
options: () => exports.options,
patch: () => exports.patch,
post: () => exports.post,
put: () => exports.put,
request: () => exports.request,
updateDefaultInstance: () => updateDefaultInstance
});
function updateDefaultInstance(config = {}) {
defaultInstance = createFetch(config);
}
var defaultInstance; exports.request = void 0; exports.get = void 0; exports.post = void 0; exports.put = void 0; exports.del = void 0; exports.patch = void 0; exports.head = void 0; exports.options = void 0; exports.ntFetch = void 0; exports.interceptors = void 0; var defaultInstanceProxy; exports.defaultInstance = void 0;
var init_fetch = __esm({
"src/fetch.ts"() {
init_client();
defaultInstance = createFetch();
exports.request = (...args) => defaultInstance.request(...args);
exports.get = (...args) => defaultInstance.get(...args);
exports.post = (...args) => defaultInstance.post(...args);
exports.put = (...args) => defaultInstance.put(...args);
exports.del = (...args) => defaultInstance.delete(...args);
exports.patch = (...args) => defaultInstance.patch(...args);
exports.head = (...args) => defaultInstance.head(...args);
exports.options = (...args) => defaultInstance.options(...args);
exports.ntFetch = new Proxy({}, {
get: (_, prop) => defaultInstance.defaults[prop],
set: (_, prop, value) => {
defaultInstance.defaults[prop] = value;
return true;
}
});
exports.interceptors = new Proxy({}, {
get: (_, prop) => defaultInstance.interceptors[prop]
});
defaultInstanceProxy = new Proxy({}, {
get: (_, prop) => defaultInstance[prop]
});
exports.defaultInstance = defaultInstanceProxy;
}
});
// src/index.ts
init_types();
init_client();
init_response();
init_error();
init_interceptors();
init_fetch();
var ERROR_MESSAGES = {
BOTH_APPROACHES: "QueryConfig cannot have both 'queryFn' and 'url' at the same time. Choose either custom function approach (queryFn) or URL-based approach (url).",
MISSING_APPROACHES: "QueryConfig must have either 'queryFn' or 'url'. Provide either a custom function or URL-based configuration."
};
function validateQueryConfig(config) {
if (process.env.NODE_ENV === "production") {
return;
}
const hasQueryFn = compat.isFunction(config.queryFn);
const hasUrl = compat.isFunction(config.url) || compat.isString(config.url);
if (hasQueryFn && hasUrl) {
throw new Error(ERROR_MESSAGES.BOTH_APPROACHES);
}
if (!hasQueryFn && !hasUrl) {
throw new Error(ERROR_MESSAGES.MISSING_APPROACHES);
}
}
function createQueryFactory(defs) {
Object.entries(defs).forEach(([key, config]) => {
try {
validateQueryConfig(config);
} catch (error) {
throw new Error(`Invalid QueryConfig for '${key}': ${error.message}`);
}
});
return defs;
}
var ERROR_MESSAGES2 = {
BOTH_APPROACHES: "MutationConfig cannot have both 'mutationFn' and 'url'+'method' at the same time. Choose either custom function approach (mutationFn) or URL-based approach (url + method).",
MISSING_APPROACHES: "MutationConfig must have either 'mutationFn' or both 'url' and 'method'. Provide either a custom function or URL-based configuration."
};
function validateMutationConfig(config) {
if (process.env.NODE_ENV === "production") {
return;
}
const hasMutationFn = compat.isFunction(config.mutationFn);
const hasUrlMethod = config.url && config.method;
if (hasMutationFn && hasUrlMethod) {
throw new Error(ERROR_MESSAGES2.BOTH_APPROACHES);
}
if (!hasMutationFn && !hasUrlMethod) {
throw new Error(ERROR_MESSAGES2.MISSING_APPROACHES);
}
}
function createMutationFactory(defs) {
Object.entries(defs).forEach(([key, config]) => {
try {
validateMutationConfig(config);
} catch (error) {
throw new Error(`Invalid MutationConfig for '${key}': ${error.message}`);
}
});
return defs;
}
function serializeQueryKey(key) {
if (compat.isString(key)) return key;
return JSON.stringify(key, (_, value) => {
if (compat.isFunction(value)) return void 0;
return value;
});
}
var QueryCache = class {
constructor(options2 = {}) {
this.subscribers = /* @__PURE__ */ new Map();
this.listeners = /* @__PURE__ */ new Map();
this.gcTimers = /* @__PURE__ */ new Map();
const { maxQueries = 1e3 } = options2;
this.cache = new QuickLRU__default.default({
maxSize: maxQueries,
onEviction: (key, value) => {
this.cleanupMetadata(key);
}
});
}
set(key, state) {
const sKey = serializeQueryKey(key);
this.cache.set(sKey, state);
this.notifyListeners(sKey);
}
get(key) {
const result = this.cache.get(serializeQueryKey(key));
return result;
}
has(key) {
const result = this.cache.has(serializeQueryKey(key));
return result;
}
delete(key) {
const sKey = serializeQueryKey(key);
this.cache.delete(sKey);
this.cleanupMetadata(sKey);
}
clear() {
this.cache.clear();
this.subscribers.clear();
this.listeners.clear();
this.gcTimers.forEach((timer) => {
clearTimeout(timer);
});
this.gcTimers.clear();
}
getAll() {
const result = {};
for (const [key, value] of this.cache.entries()) {
result[key] = value;
}
return result;
}
/**
* 컴포넌트가 쿼리를 구독하여 refetch 콜백을 등록합니다.
* @returns unsubscribe 함수
*/
subscribeListener(key, listener) {
const sKey = serializeQueryKey(key);
if (!this.listeners.has(sKey)) {
this.listeners.set(sKey, /* @__PURE__ */ new Set());
}
this.listeners.get(sKey).add(listener);
return () => {
const listenerSet = this.listeners.get(sKey);
if (listenerSet) {
listenerSet.delete(listener);
if (listenerSet.size === 0) {
this.listeners.delete(sKey);
}
}
};
}
/**
* 특정 쿼리 키의 모든 리스너에게 알림을 보냅니다.
*/
notifyListeners(key) {
const sKey = serializeQueryKey(key);
Promise.resolve().then(() => {
this.listeners.get(sKey)?.forEach((l) => l());
});
}
/**
* 구독자 수 증가 및 gcTime 타이머 해제 (생명주기 관리)
*/
subscribe(key) {
const sKey = serializeQueryKey(key);
const prev = this.subscribers.get(sKey) ?? 0;
this.subscribers.set(sKey, prev + 1);
const timer = this.gcTimers.get(sKey);
if (timer) {
clearTimeout(timer);
this.gcTimers.delete(sKey);
}
}
/**
* 구독자 수 감소 및 0이 되면 gcTime 타이머 시작 (생명주기 관리)
*/
unsubscribe(key, gcTime) {
const sKey = serializeQueryKey(key);
const prev = this.subscribers.get(sKey) ?? 0;
if (prev <= 1) {
this.subscribers.set(sKey, 0);
if (gcTime !== Infinity) {
const timer = setTimeout(() => {
this.delete(key);
}, gcTime);
this.gcTimers.set(sKey, timer);
}
} else {
this.subscribers.set(sKey, prev - 1);
}
}
serialize() {
return this.getAll();
}
deserialize(cache) {
Object.entries(cache).forEach(([key, state]) => {
this.cache.set(key, state);
});
}
/**
* 현재 캐시 크기를 반환합니다.
*/
get size() {
return this.cache.size;
}
/**
* 캐시의 최대 크기를 반환합니다.
*/
get maxSize() {
return this.cache.maxSize;
}
/**
* 캐시 통계를 반환합니다.
*
* @description 디버깅 및 모니터링 목적으로 사용됩니다.
* 성능 분석, 메모리 사용량 추적, 캐시 상태 확인 등에 활용할 수 있습니다.
*
* @example
* ```typescript
* const queryClient = useQueryClient();
* const stats = queryClient.getQueryCache().getStats();
* console.log('Current cache size:', stats.cacheSize);
* console.log('Active GC timers:', stats.activeGcTimersCount);
* ```
*/
getStats() {
return {
/** 현재 캐시된 쿼리 수 */
cacheSize: this.cache.size,
/** 최대 쿼리 수 (메모리 보호 한계) */
maxSize: this.cache.maxSize,
/** 활성 구독자 수 */
subscribersCount: this.subscribers.size,
/** 등록된 리스너 수 */
listenersCount: this.listeners.size,
/** 활성 GC 타이머 수 (생명주기 관리 중인 쿼리) */
activeGcTimersCount: this.gcTimers.size
};
}
/**
* 특정 키의 메타데이터를 정리합니다.
*/
cleanupMetadata(sKey) {
this.subscribers.delete(sKey);
this.listeners.delete(sKey);
const timer = this.gcTimers.get(sKey);
if (timer) {
clearTimeout(timer);
this.gcTimers.delete(sKey);
}
}
};
// src/query/client/query-client.ts
init_client();
var QueryClient = class {
constructor(options2) {
this.cache = new QueryCache(options2?.queryCache);
this.fetcher = options2?.fetcher || createFetch(options2);
}
has(key) {
return this.cache.has(key);
}
getFetcher() {
return this.fetcher;
}
/**
* 쿼리 상태 조회
*/
get(key) {
return this.cache.get(key);
}
/**
* 쿼리 상태 저장
*/
set(key, state) {
this.cache.set(key, state);
}
/**
* 쿼리 데이터만 업데이트 (optimistic update에 최적화)
* 기존 상태(isLoading, isFetching, error)를 유지하면서 data와 updatedAt만 업데이트
*/
setQueryData(key, updater) {
const existing = this.get(key);
const newData = typeof updater === "function" ? updater(existing?.data) : updater;
const newState = {
data: newData,
error: existing?.error,
isLoading: existing?.isLoading ?? false,
isFetching: existing?.isFetching ?? false,
updatedAt: Date.now()
};
this.set(key, newState);
}
/**
* 쿼리 상태 삭제
*/
delete(key) {
this.cache.delete(key);
}
/**
* 모든 쿼리 상태 반환
*/
getAll() {
return this.cache.getAll();
}
/**
* 모든 쿼리 상태 초기화
*/
clear() {
this.cache.clear();
}
/**
* 특정 쿼리키(혹은 prefix)로 시작하는 모든 쿼리 캐시를 무효화(삭제)
* 예: invalidateQueries(['user']) → ['user', ...]로 시작하는 모든 캐시 삭제
*/
invalidateQueries(prefix) {
const all = this.getAll();
if (compat.isArray(prefix)) {
const prefixArr = Array.from(prefix);
compat.forEach(Object.keys(all), (key) => {
try {
const keyArr = JSON.parse(key);
if (Array.isArray(keyArr) && compat.isEqual(keyArr.slice(0, prefixArr.length), prefixArr)) {
const currentState = this.cache.get(keyArr);
if (currentState) {
this.cache.set(keyArr, { ...currentState, updatedAt: 0 });
}
}
} catch {
}
});
} else {
const prefixStr = compat.isString(prefix) ? prefix : String(prefix);
compat.forEach(Object.keys(all), (key) => {
if (key.startsWith(prefixStr)) {
const currentState = this.cache.get(key);
if (currentState) {
this.cache.set(key, { ...currentState, updatedAt: 0 });
}
}
});
}
}
/**
* 구독자 관리 (public)
*/
subscribeListener(key, listener) {
return this.cache.subscribeListener(key, listener);
}
subscribe(key) {
this.cache.subscribe(key);
}
unsubscribe(key, gcTime) {
this.cache.unsubscribe(key, gcTime);
}
// 구현
async prefetchQuery(keyOrQuery, fetchFnOrParams) {
if (typeof keyOrQuery === "object" && keyOrQuery && "cacheKey" in keyOrQuery) {
const query = keyOrQuery;
const params = fetchFnOrParams;
const cacheKey = query.cacheKey(params);
const fetchFn2 = async () => {
let data2;
if (query.queryFn) {
data2 = await query.queryFn(params, this.fetcher);
} else if (query.url) {
const url = query.url(params);
const response = await this.fetcher.get(url, query.fetchConfig);
data2 = response.data;
} else {
throw new Error("Either 'url' or 'queryFn' must be provided in QueryConfig");
}
if (query.schema) {
data2 = query.schema.parse(data2);
}
if (query.select) {
data2 = query.select(data2);
}
return data2;
};
return this.prefetchQuery(cacheKey, fetchFn2);
}
const key = keyOrQuery;
const fetchFn = fetchFnOrParams;
const data = await fetchFn();
this.set(key, {
data,
error: void 0,
isLoading: false,
isFetching: false,
updatedAt: Date.now()
});
return data;
}
dehydrate() {
return this.cache.serialize();
}
hydrate(cache) {
this.cache.deserialize(cache);
}
/**
* 캐시 통계를 반환합니다. (디버깅 목적)
*
* @description 성능 분석, 메모리 사용량 추적, 캐시 상태 확인 등에 활용할 수 있습니다.
*/
getQueryCache() {
return this.cache;
}
};
// src/query/client/query-client-manager.ts
var globalQueryClient = void 0;
var defaultOptions = void 0;
function setDefaultQueryClientOptions(options2) {
defaultOptions = options2;
updateGlobalFetchInstance(options2);
if (typeof window !== "undefined" && globalQueryClient) {
globalQueryClient = createQueryClientWithSetup(options2);
}
}
function updateGlobalFetchInstance(options2) {
Promise.resolve().then(() => (init_fetch(), fetch_exports)).then(({ updateDefaultInstance: updateDefaultInstance2 }) => {
const { setupInterceptors, ...fetchConfig } = options2;
updateDefaultInstance2(fetchConfig);
if (setupInterceptors) {
Promise.resolve().then(() => (init_fetch(), fetch_exports)).then(({ interceptors: interceptors2 }) => {
const dummyFetcher = { interceptors: interceptors2 };
setupInterceptors(dummyFetcher);
});
}
});
}
function createQueryClientWithSetup(options2) {
if (!options2?.setupInterceptors) {
return new QueryClient(options2);
}
const { setupInterceptors, ...clientOptions } = options2;
const client = new QueryClient(clientOptions);
setupInterceptors(client.getFetcher());
return client;
}
function getQueryClient(options2) {
const finalOptions = defaultOptions ? { ...defaultOptions, ...options2 } : options2;
if (typeof window === "undefined") {
return createQueryClientWithSetup(finalOptions);
}
if (!globalQueryClient) {
globalQueryClient = createQueryClientWithSetup(finalOptions);
}
return globalQueryClient;
}
function resetQueryClient() {
if (typeof window !== "undefined") {
globalQueryClient = void 0;
}
}
function createQueryClientWithInterceptors(options2, setupInterceptors) {
return createQueryClientWithSetup({
...options2,
setupInterceptors
});
}
function replaceEqualDeep(prev, next) {
if (prev === next) {
return prev;
}
const prevType = typeof prev;
const nextType = typeof next;
if (prevType !== nextType) {
return next;
}
if (prevType !== "object" || prev === null || next === null) {