UNPKG

next-unified-query-core

Version:

Framework-agnostic HTTP client with built-in query state management

1,671 lines (1,654 loc) 88.3 kB
'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) {