fexios
Version:
Fetch based HTTP client with similar API to axios for browser and Node.js
395 lines (391 loc) • 12.8 kB
JavaScript
'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