UNPKG

fexios

Version:

Fetch based HTTP client with similar API to axios for browser and Node.js

395 lines (391 loc) 12.8 kB
'use strict'; const utils_index = require('./utils/index.cjs'); const models_index = require('./models/index.cjs'); const isPlainObject = require('./shared/fexios.C6EYiGQl.cjs'); class Fexios extends utils_index.CallableInstance { static version = "5.3.1"; static FINAL_SYMBOL = Symbol("FEXIOS_FINAL_CONTEXT"); baseConfigs; // for axios compatibility get defaults() { return this.baseConfigs; } set defaults(configs) { this.baseConfigs = configs; } static DEFAULT_CONFIGS = { baseURL: "", timeout: 0, credentials: void 0, headers: {}, query: {}, responseType: void 0, shouldThrow(response) { return !response.ok; }, fetch: globalThis.fetch }; hooks = []; static ALL_METHODS = [ "get", "post", "put", "patch", "delete", "head", "options", "trace" ]; static METHODS_WITHOUT_BODY = [ "get", "head", "options", "trace" ]; constructor(baseConfigs = {}) { super("request"); this.baseConfigs = utils_index.deepMerge(Fexios.DEFAULT_CONFIGS, baseConfigs); Fexios.ALL_METHODS.forEach( (m) => this.createMethodShortcut(m.toLowerCase()) ); } async request(urlOrOptions, options) { let ctx = options || {}; if (typeof urlOrOptions === "string" || urlOrOptions instanceof URL) { ctx.url = urlOrOptions.toString(); } else if (typeof urlOrOptions === "object") { ctx = urlOrOptions; } ctx = await this.emit("beforeInit", ctx); if (ctx[Fexios.FINAL_SYMBOL]) return ctx; ctx = this.applyDefaults(ctx); if (Fexios.METHODS_WITHOUT_BODY.includes( ctx.method?.toLocaleLowerCase() ) && ctx.body) { throw new models_index.FexiosError( models_index.FexiosErrorCodes.BODY_NOT_ALLOWED, `Request method "${ctx.method}" does not allow body` ); } ctx = await this.emit("beforeRequest", ctx); if (ctx[Fexios.FINAL_SYMBOL]) return ctx; let body; const headerAutoPatch = {}; if (typeof ctx.body !== "undefined" && ctx.body !== null) { if (ctx.body instanceof Blob || ctx.body instanceof FormData || ctx.body instanceof URLSearchParams) { body = ctx.body; } else if (typeof ctx.body === "object" && ctx.body !== null) { body = JSON.stringify(ctx.body); ctx.headers = this.mergeHeaders(ctx.headers, { "Content-Type": "application/json" }); } else { body = ctx.body; } } const optionsHeaders = models_index.FexiosHeaderBuilder.makeHeaders(ctx.headers || {}); if (!optionsHeaders.get("content-type") && body) { if (body instanceof FormData || body instanceof URLSearchParams) { headerAutoPatch["content-type"] = null; } else if (typeof body === "string" && typeof ctx.body === "object") { headerAutoPatch["content-type"] = "application/json"; } else if (body instanceof Blob) { headerAutoPatch["content-type"] = body.type || "application/octet-stream"; } } ctx.body = body; ctx = await this.emit("afterBodyTransformed", ctx); if (ctx[Fexios.FINAL_SYMBOL]) return ctx; const abortController = ctx.abortController ?? (globalThis.AbortController ? new AbortController() : void 0); const fallback = globalThis.location?.href || "http://localhost"; const baseForRequest = new URL( ctx.baseURL || this.baseConfigs.baseURL || fallback, fallback ); const urlObjForRequest = new URL(ctx.url, baseForRequest); const finalURLForRequest = models_index.FexiosQueryBuilder.makeURL( urlObjForRequest, ctx.query, urlObjForRequest.hash // 保留 hash ).toString(); const rawRequest = new Request(finalURLForRequest, { method: ctx.method || "GET", credentials: ctx.credentials, cache: ctx.cache, mode: ctx.mode, headers: models_index.FexiosHeaderBuilder.mergeHeaders( this.baseConfigs.headers, ctx.headers || {}, headerAutoPatch ), body: ctx.body, signal: abortController?.signal }); ctx.rawRequest = rawRequest; ctx = await this.emit("beforeActualFetch", ctx); if (ctx[Fexios.FINAL_SYMBOL]) return ctx; const timeout = ctx.timeout ?? this.baseConfigs.timeout ?? 60 * 1e3; const shouldThrow = ctx.shouldThrow ?? this.baseConfigs.shouldThrow; if (ctx.url.startsWith("ws") || ctx.responseType === "ws") { const response = await models_index.createFexiosWebSocketResponse( ctx.url, void 0, timeout ); const finalCtx = { ...ctx, response, rawResponse: void 0, data: response.data, headers: response.headers }; return this.emit("afterResponse", finalCtx); } let timer; try { if (abortController) { timer = timeout > 0 ? setTimeout(() => { abortController.abort(); }, timeout) : void 0; } const fetch = ctx.fetch || this.baseConfigs.fetch || globalThis.fetch; const rawResponse = await fetch(ctx.rawRequest).catch((err) => { if (timer) clearTimeout(timer); if (abortController?.signal.aborted) { throw new models_index.FexiosError( models_index.FexiosErrorCodes.TIMEOUT, `Request timed out after ${timeout}ms`, ctx ); } throw new models_index.FexiosError(models_index.FexiosErrorCodes.NETWORK_ERROR, err.message, ctx); }); if (timer) clearTimeout(timer); ctx.rawResponse = rawResponse; ctx.response = await models_index.createFexiosResponse( rawResponse, ctx.responseType, (progress, buffer) => { options?.onProgress?.(progress, buffer); }, shouldThrow, timeout ); ctx.rawResponse = ctx.response.rawResponse; Object.defineProperties(ctx, { url: { get: () => ctx.rawResponse?.url || finalURLForRequest }, data: { get: () => ctx.response.data }, headers: { get: () => ctx.rawResponse.headers }, responseType: { get: () => ctx.response.responseType } }); return this.emit("afterResponse", ctx); } catch (error) { if (timer) clearTimeout(timer); throw error; } } mergeQueries = models_index.FexiosQueryBuilder.mergeQueries; mergeHeaders = models_index.FexiosHeaderBuilder.mergeHeaders; applyDefaults(ctx) { const c = ctx; if ("customEnv" in this.baseConfigs) { c.customEnv = utils_index.deepMerge( {}, // ensure we don't mutate baseConfigs this.baseConfigs.customEnv, c.customEnv ); } const fallback = globalThis.location?.href || "http://localhost"; const effectiveBase = c.baseURL || this.baseConfigs.baseURL || fallback; const baseObj = new URL(effectiveBase, fallback); const reqURL = new URL(c.url.toString(), baseObj); const baseSearchParams = models_index.FexiosQueryBuilder.toQueryRecord( baseObj.searchParams ); const reqSearchParams = models_index.FexiosQueryBuilder.toQueryRecord( reqURL.searchParams ); const mergedSearchParams = models_index.FexiosQueryBuilder.mergeQueries( baseSearchParams, reqSearchParams ); reqURL.search = models_index.FexiosQueryBuilder.makeSearchParams(mergedSearchParams).toString(); c.url = reqURL.toString(); const mergedQuery = models_index.FexiosQueryBuilder.mergeQueries( this.baseConfigs.query, c.query ); if (c.query) { this.restoreNulls(mergedQuery, c.query); } c.query = mergedQuery; return c; } restoreNulls(target, source) { if (!source || typeof source !== "object") return; for (const [k, v] of Object.entries(source)) { if (v === null) { target[k] = null; } else if (isPlainObject.isPlainObject(v)) { if (!target[k] || typeof target[k] !== "object") { target[k] = {}; } this.restoreNulls(target[k], v); } } } async emit(event, ctx, opts = { shouldHandleShortCircuitResponse: true }) { const hooks = this.hooks.filter((h) => h.event === event); if (hooks.length === 0) return ctx; const shortCircuit = async (baseCtx, raw) => { const finalCtx = { ...baseCtx, rawResponse: raw }; const response = await models_index.createFexiosResponse( raw, baseCtx.responseType, (progress, buffer) => baseCtx.onProgress?.(progress, buffer), baseCtx.shouldThrow ?? this.baseConfigs.shouldThrow, baseCtx.timeout ?? this.baseConfigs.timeout ?? 60 * 1e3 ); finalCtx.response = response; finalCtx.rawResponse = response.rawResponse; finalCtx.data = response.data; finalCtx.headers = response.headers; if (event !== "afterResponse") { const after = await this.emit("afterResponse", finalCtx); after[Fexios.FINAL_SYMBOL] = true; return after; } else { finalCtx[Fexios.FINAL_SYMBOL] = true; return finalCtx; } }; for (let i = 0; i < hooks.length; i++) { const hook = hooks[i]; const hookName = `${String(event)}#${hook.action.name || `anonymous#${i}`}`; const marker = Symbol("FEXIOS_HOOK_CTX_MARK"); try { ; ctx[marker] = marker; } catch { } const result = await hook.action.call(this, ctx); try { delete ctx[marker]; } catch { } if (result === false) { throw new models_index.FexiosError( models_index.FexiosErrorCodes.ABORTED_BY_HOOK, `Request aborted by hook "${hookName}"`, ctx ); } if (result instanceof Response) { if (opts.shouldHandleShortCircuitResponse !== false) { return shortCircuit(ctx, result); } ctx.rawResponse = result; } else if (result && typeof result === "object" && result[marker] === marker) { ctx = result; } else ; } return ctx; } on(event, action, prepend = false) { if (typeof action !== "function") { throw new models_index.FexiosError( models_index.FexiosErrorCodes.INVALID_HOOK_CALLBACK, `Hook should be a function, but got "${typeof action}"` ); } this.hooks[prepend ? "unshift" : "push"]({ event, action }); return this; } off(event, action) { if (event === "*" || !event) { this.hooks = this.hooks.filter((hook) => hook.action !== action); } else { this.hooks = this.hooks.filter( (hook) => hook.event !== event || hook.action !== action ); } return this; } createInterceptor(event) { return { handlers: () => this.hooks.filter((hook) => hook.event === event).map((hook) => hook.action), use: (hook, prepend = false) => { return this.on(event, hook, prepend); }, clear: () => { this.hooks = this.hooks.filter((hook) => hook.event !== event); } }; } interceptors = { request: this.createInterceptor("beforeRequest"), response: this.createInterceptor("afterResponse") }; createMethodShortcut(method) { Reflect.defineProperty(this, method, { get: () => { return (url, bodyOrQuery, options) => { if (Fexios.METHODS_WITHOUT_BODY.includes( method.toLocaleLowerCase() )) { options = bodyOrQuery; } else { options = options || {}; options.body = bodyOrQuery; } return this.request(url, { ...options, method }); }; } }); return this; } extends(configs) { const fexios = new Fexios(utils_index.deepMerge(this.baseConfigs, configs)); fexios.hooks = [...this.hooks]; fexios._plugins = new Map(this._plugins); fexios._plugins.forEach(async (plugin) => { await fexios.plugin(plugin); }); return fexios; } create = Fexios.create; static create(configs) { return new Fexios(configs); } _plugins = /* @__PURE__ */ new Map(); async plugin(plugin) { if (typeof plugin?.name === "string" && typeof plugin?.install === "function") { if (this._plugins.has(plugin.name)) { return this; } const fx = await plugin.install(this); this._plugins.set(plugin.name, plugin); if (fx instanceof Fexios) { return fx; } } return this; } // 版本弃子们.jpg /** @deprecated Use `import { checkIsPlainObject } from 'fexios/utils'` instead */ checkIsPlainObject = isPlainObject.isPlainObject; /** @deprecated Use `mergeQueries` instead */ mergeQuery = this.mergeQueries; } exports.Fexios = Fexios; //# sourceMappingURL=fexios.cjs.map