alova
Version:
The Request Toolkit For Ultimate Efficiency
1,030 lines (1,015 loc) • 202 kB
JavaScript
/**
* @alova/client 2.0.0 (https://alova.js.org)
* Document https://alova.js.org
* Copyright 2025 Scott hu. All Rights Reserved
* Licensed under MIT (git://github.com/alovajs/alova/blob/main/LICENSE)
*/
'use strict';
var shared = require('@alova/shared');
var alova = require('alova');
const defaultVisitorMeta = {
authRole: null
};
const defaultLoginMeta = {
authRole: 'login'
};
const defaultLogoutMeta = {
authRole: 'logout'
};
const defaultRefreshTokenMeta = {
authRole: 'refreshToken'
};
const checkMethodRole = ({ meta }, metaMatches) => {
if (shared.isPlainObject(meta)) {
for (const key in meta) {
if (Object.prototype.hasOwnProperty.call(meta, key)) {
const matchedMetaItem = metaMatches[key];
if (shared.instanceOf(matchedMetaItem, RegExp) ? matchedMetaItem.test(meta[key]) : meta[key] === matchedMetaItem) {
return shared.trueValue;
}
}
}
}
return shared.falseValue;
};
const waitForTokenRefreshed = (method, waitingList) => shared.newInstance(shared.PromiseCls, resolve => {
shared.pushItem(waitingList, {
method,
resolve
});
});
const callHandlerIfMatchesMeta = (method, authorizationInterceptor, defaultMeta, response) => {
if (checkMethodRole(method, (authorizationInterceptor === null || authorizationInterceptor === void 0 ? void 0 : authorizationInterceptor.metaMatches) || defaultMeta)) {
const handler = shared.isFn(authorizationInterceptor)
? authorizationInterceptor
: shared.isPlainObject(authorizationInterceptor) && shared.isFn(authorizationInterceptor.handler)
? authorizationInterceptor.handler
: shared.noop;
return handler(response, method);
}
};
const refreshTokenIfExpired = async (method, waitingList, updateRefreshStatus, handlerParams, refreshToken, tokenRefreshing) => {
// When the number of handle params is greater than 2, it means that this function is called from the response, and the original interface needs to be requested again.
const fromResponse = shared.len(handlerParams) >= 2;
let isExpired = refreshToken === null || refreshToken === void 0 ? void 0 : refreshToken.isExpired(...handlerParams);
// Compatible with synchronous and asynchronous functions
if (shared.instanceOf(isExpired, shared.PromiseCls)) {
isExpired = await isExpired;
}
if (isExpired) {
try {
// Make another judgment in the response to prevent multiple requests to refresh the token, intercept and wait for the token sent before the token refresh is completed.
let intentToRefreshToken = shared.trueValue;
if (fromResponse && tokenRefreshing) {
intentToRefreshToken = shared.falseValue; // The requests waiting here indicate that the token is being refreshed. When they pass, there is no need to refresh the token again.
await waitForTokenRefreshed(method, waitingList);
}
if (intentToRefreshToken) {
updateRefreshStatus(shared.trueValue);
// Call refresh token
await (refreshToken === null || refreshToken === void 0 ? void 0 : refreshToken.handler(...handlerParams));
updateRefreshStatus(shared.falseValue);
// After the token refresh is completed, the requests in the waiting list are notified.
shared.forEach(waitingList, ({ resolve }) => resolve());
}
if (fromResponse) {
// Because the original interface is being requested again, superposition with the previous request will result in repeated calls to transform, so it is necessary to leave transform empty to remove one call.
const { config } = method;
const methodTransformData = config.transform;
config.transform = shared.undefinedValue;
const resentData = await method;
config.transform = methodTransformData;
return resentData;
}
}
finally {
updateRefreshStatus(shared.falseValue);
shared.splice(waitingList, 0, shared.len(waitingList)); // Clear waiting list
}
}
};
const onResponded2Record = (onRespondedHandlers) => {
let successHandler = shared.undefinedValue;
let errorHandler = shared.undefinedValue;
let onCompleteHandler = shared.undefinedValue;
if (shared.isFn(onRespondedHandlers)) {
successHandler = onRespondedHandlers;
}
else if (shared.isPlainObject(onRespondedHandlers)) {
const { onSuccess, onError, onComplete } = onRespondedHandlers;
successHandler = shared.isFn(onSuccess) ? onSuccess : successHandler;
errorHandler = shared.isFn(onError) ? onError : errorHandler;
onCompleteHandler = shared.isFn(onComplete) ? onComplete : onCompleteHandler;
}
return {
onSuccess: successHandler,
onError: errorHandler,
onComplete: onCompleteHandler
};
};
/**
* Create a client-side token authentication interceptor
* @param options Configuration parameters
* @returns token authentication interceptor function
*/
const createClientTokenAuthentication = ({ visitorMeta, login, logout, refreshToken, assignToken = shared.noop }) => {
let tokenRefreshing = shared.falseValue;
const waitingList = [];
const onAuthRequired = onBeforeRequest => async (method) => {
const isVisitorRole = checkMethodRole(method, visitorMeta || defaultVisitorMeta);
const isLoginRole = checkMethodRole(method, (login === null || login === void 0 ? void 0 : login.metaMatches) || defaultLoginMeta);
// Ignored, login, and token refresh requests do not perform token authentication.
if (!isVisitorRole &&
!isLoginRole &&
!checkMethodRole(method, (refreshToken === null || refreshToken === void 0 ? void 0 : refreshToken.metaMatches) || defaultRefreshTokenMeta)) {
// If the token is being refreshed, wait for the refresh to complete before sending a request.
if (tokenRefreshing) {
await waitForTokenRefreshed(method, waitingList);
}
await refreshTokenIfExpired(method, waitingList, refreshing => {
tokenRefreshing = refreshing;
}, [method], refreshToken);
}
// Requests from non-guest and logged-in roles will enter the assignment token function
if (!isVisitorRole && !isLoginRole) {
await assignToken(method);
}
return onBeforeRequest === null || onBeforeRequest === void 0 ? void 0 : onBeforeRequest(method);
};
const onResponseRefreshToken = originalResponded => {
const respondedRecord = onResponded2Record(originalResponded);
return {
...respondedRecord,
onSuccess: async (response, method) => {
await callHandlerIfMatchesMeta(method, login, defaultLoginMeta, response);
await callHandlerIfMatchesMeta(method, logout, defaultLogoutMeta, response);
return (respondedRecord.onSuccess || shared.$self)(response, method);
}
};
};
return {
waitingList,
onAuthRequired,
onResponseRefreshToken
};
};
/**
* Create a server-side token authentication interceptor
* @param options Configuration parameters
* @returns token authentication interceptor function
*/
const createServerTokenAuthentication = ({ visitorMeta, login, logout, refreshTokenOnSuccess, refreshTokenOnError, assignToken = shared.noop }) => {
let tokenRefreshing = shared.falseValue;
const waitingList = [];
const onAuthRequired = onBeforeRequest => async (method) => {
const isVisitorRole = checkMethodRole(method, visitorMeta || defaultVisitorMeta);
const isLoginRole = checkMethodRole(method, (login === null || login === void 0 ? void 0 : login.metaMatches) || defaultLoginMeta);
// Ignored, login, and token refresh requests do not perform token authentication.
if (!isVisitorRole &&
!isLoginRole &&
!checkMethodRole(method, (refreshTokenOnSuccess === null || refreshTokenOnSuccess === void 0 ? void 0 : refreshTokenOnSuccess.metaMatches) || defaultRefreshTokenMeta) &&
!checkMethodRole(method, (refreshTokenOnError === null || refreshTokenOnError === void 0 ? void 0 : refreshTokenOnError.metaMatches) || defaultRefreshTokenMeta)) {
// If the token is being refreshed, wait for the refresh to complete before sending a request.
if (tokenRefreshing) {
await waitForTokenRefreshed(method, waitingList);
}
}
if (!isVisitorRole && !isLoginRole) {
await assignToken(method);
}
return onBeforeRequest === null || onBeforeRequest === void 0 ? void 0 : onBeforeRequest(method);
};
const onResponseRefreshToken = onRespondedHandlers => {
const respondedRecord = onResponded2Record(onRespondedHandlers);
return {
...respondedRecord,
onSuccess: async (response, method) => {
if (!checkMethodRole(method, visitorMeta || defaultVisitorMeta) &&
!checkMethodRole(method, (login === null || login === void 0 ? void 0 : login.metaMatches) || defaultLoginMeta) &&
!checkMethodRole(method, (refreshTokenOnSuccess === null || refreshTokenOnSuccess === void 0 ? void 0 : refreshTokenOnSuccess.metaMatches) || defaultRefreshTokenMeta)) {
const dataResent = await refreshTokenIfExpired(method, waitingList, refreshing => {
tokenRefreshing = refreshing;
}, [response, method], refreshTokenOnSuccess, tokenRefreshing);
if (dataResent) {
return dataResent;
}
}
await callHandlerIfMatchesMeta(method, login, defaultLoginMeta, response);
await callHandlerIfMatchesMeta(method, logout, defaultLogoutMeta, response);
return (respondedRecord.onSuccess || shared.$self)(response, method);
},
onError: async (error, method) => {
if (!checkMethodRole(method, visitorMeta || defaultVisitorMeta) &&
!checkMethodRole(method, (login === null || login === void 0 ? void 0 : login.metaMatches) || defaultLoginMeta) &&
!checkMethodRole(method, (refreshTokenOnError === null || refreshTokenOnError === void 0 ? void 0 : refreshTokenOnError.metaMatches) || defaultRefreshTokenMeta)) {
const dataResent = await refreshTokenIfExpired(method, waitingList, refreshing => {
tokenRefreshing = refreshing;
}, [error, method], refreshTokenOnError, tokenRefreshing);
if (dataResent) {
return dataResent;
}
}
return (respondedRecord.onError || shared.noop)(error, method);
}
};
};
return {
waitingList,
onAuthRequired,
onResponseRefreshToken
};
};
/**
* Compatible functions, throwing parameters
* @param error mistake
*/
const throwFn = (error) => {
throw error;
};
function useCallback(onCallbackChange = shared.noop) {
let callbacks = [];
const setCallback = (fn) => {
if (!callbacks.includes(fn)) {
callbacks.push(fn);
onCallbackChange(callbacks);
}
// Return unregister function
return () => {
callbacks = shared.filterItem(callbacks, e => e !== fn);
onCallbackChange(callbacks);
};
};
const triggerCallback = (...args) => {
if (callbacks.length > 0) {
return shared.forEach(callbacks, fn => fn(...args));
}
};
const removeAllCallback = () => {
callbacks = [];
onCallbackChange(callbacks);
};
return [setCallback, triggerCallback, removeAllCallback];
}
/**
* Create a debounce function and trigger the function immediately when delay is 0
* Scenario: When calling useWatcher and setting immediate to true, the first call must be executed immediately, otherwise it will cause a delayed call
* @param {GeneralFn} fn callback function
* @param {number|(...args: any[]) => number} delay Delay description, dynamic delay can be achieved when set as a function
* @returns Delayed callback function
*/
const debounce = (fn, delay) => {
let timer = shared.nullValue;
return function debounceFn(...args) {
const bindFn = fn.bind(this, ...args);
const delayMill = shared.isNumber(delay) ? delay : delay(...args);
timer && shared.clearTimeoutTimer(timer);
if (delayMill > 0) {
timer = shared.setTimeoutFn(bindFn, delayMill);
}
else {
bindFn();
}
};
};
/**
* Get the request method object
* @param methodHandler Request method handle
* @param args Method call parameters
* @returns request method object
*/
const getHandlerMethod = (methodHandler, args = []) => {
const methodInstance = shared.isFn(methodHandler) ? methodHandler(...args) : methodHandler;
const assert = shared.createAssert('scene');
assert(shared.instanceOf(methodInstance, alova.Method), 'hook handler must be a method instance or a function that returns method instance');
return methodInstance;
};
/**
* Convert each value of the object and return the new object
* @param obj object
* @param callback callback function
* @returns converted object
*/
const mapObject = (obj, callback) => {
const ret = {};
for (const key in obj) {
ret[key] = callback(obj[key], key, obj);
}
return ret;
};
var EnumHookType;
(function (EnumHookType) {
EnumHookType[EnumHookType["USE_REQUEST"] = 1] = "USE_REQUEST";
EnumHookType[EnumHookType["USE_WATCHER"] = 2] = "USE_WATCHER";
EnumHookType[EnumHookType["USE_FETCHER"] = 3] = "USE_FETCHER";
})(EnumHookType || (EnumHookType = {}));
/**
* create simple and unified, framework-independent states creators and handlers.
* @param statesHook states hook from `promiseStatesHook` function of alova
* @param referingObject refering object exported from `promiseStatesHook` function
* @returns simple and unified states creators and handlers
*/
function statesHookHelper(statesHook, referingObject = {
trackedKeys: {},
bindError: shared.falseValue,
initialRequest: shared.falseValue,
...shared.injectReferingObject()
}) {
const ref = (initialValue) => (statesHook.ref ? statesHook.ref(initialValue) : { current: initialValue });
referingObject = ref(referingObject).current;
const exportState = (state) => (statesHook.export || shared.$self)(state, referingObject);
const memorize = (fn) => {
if (!shared.isFn(statesHook.memorize)) {
return fn;
}
const memorizedFn = statesHook.memorize(fn);
memorizedFn.memorized = shared.trueValue;
return memorizedFn;
};
const { dehydrate } = statesHook;
// For performance reasons, only value is different, and the key is tracked can be updated.
const update = (newValue, state, key) => newValue !== dehydrate(state, key, referingObject) &&
referingObject.trackedKeys[key] &&
statesHook.update(newValue, state, key, referingObject);
const mapDeps = (deps) => shared.mapItem(deps, item => (shared.instanceOf(item, shared.FrameworkReadableState) ? item.e : item));
const createdStateList = [];
// key of deps on computed
const depKeys = {};
return {
create: (initialValue, key) => {
shared.pushItem(createdStateList, key); // record the keys of created states.
return shared.newInstance((shared.FrameworkState), statesHook.create(initialValue, key, referingObject), key, state => dehydrate(state, key, referingObject), exportState, (state, newValue) => update(newValue, state, key));
},
computed: (getter, depList, key) => {
// Collect all dependencies in computed
shared.forEach(depList, dep => {
if (dep.k) {
depKeys[dep.k] = shared.trueValue;
}
});
return shared.newInstance((shared.FrameworkReadableState), statesHook.computed(getter, mapDeps(depList), key, referingObject), key, state => dehydrate(state, key, referingObject), exportState);
},
effectRequest: (effectRequestParams) => statesHook.effectRequest(effectRequestParams, referingObject),
ref,
watch: (source, callback) => statesHook.watch(mapDeps(source), callback, referingObject),
onMounted: (callback) => statesHook.onMounted(callback, referingObject),
onUnmounted: (callback) => statesHook.onUnmounted(callback, referingObject),
memorize,
/**
* refering object that sharing some value with this object.
*/
__referingObj: referingObject,
/**
* expose provider for specified use hook.
* @param object object that contains state proxy, framework state, operating function and event binder.
* @returns provider component.
*/
exposeProvider: (object) => {
const provider = {};
const originalStatesMap = {};
const stateKeys = [];
for (const key in object) {
const value = object[key];
const isValueFunction = shared.isFn(value);
// if it's a memorized function, don't memorize it any more, add it to provider directly.
// if it's start with `on`, that indicates it is an event binder, we should define a new function which return provider object.
// if it's a common function, add it to provider with memorize mode.
// Note that: in some situation, state is a function such as solid's signal, and state value is set to function in react, the state will be detected as a function. so we should check whether the key is in `trackedKeys`
if (isValueFunction && !referingObject.trackedKeys[key]) {
provider[key] = key.startsWith('on')
? (...args) => {
value(...args);
// eslint-disable-next-line
return completedProvider;
}
: value.memorized
? value
: memorize(value);
}
else {
// collect states of current exposures, and open tracked for these ststes
if (!shared.includes(['uploading', 'downloading'], key) && !key.startsWith('__')) {
shared.pushItem(stateKeys, key);
}
const isFrameworkState = shared.instanceOf(value, shared.FrameworkReadableState);
if (isFrameworkState) {
originalStatesMap[key] = value.s;
}
// otherwise, it's a state proxy or framework state, add it to provider with getter mode.
shared.ObjectCls.defineProperty(provider, key, {
get: () => {
// record the key that is being tracked.
referingObject.trackedKeys[key] = shared.trueValue;
return isFrameworkState ? value.e : value;
},
// set need to set an function,
// otherwise it will throw `TypeError: Cannot set property __referingObj of #<Object> which has only a getter` when setting value
set: shared.noop,
enumerable: shared.trueValue,
configurable: shared.trueValue
});
}
}
const { update: nestedHookUpdate, __proxyState: nestedProxyState } = provider;
// reset the tracked keys and bingError flag, so that the nest hook providers can be initialized.
// Always track the dependencies in computed
referingObject.trackedKeys = {
...depKeys
};
referingObject.bindError = shared.falseValue;
const { then: providerThen } = provider;
const extraProvider = {
// expose referingObject automatically.
__referingObj: referingObject,
// the new updating function that can update the new states and nested hook states.
update: memorize((newStates) => {
shared.objectKeys(newStates).forEach(key => {
if (shared.includes(createdStateList, key)) {
update(newStates[key], originalStatesMap[key], key);
}
else if (key in provider && shared.isFn(nestedHookUpdate)) {
nestedHookUpdate({
[key]: newStates[key]
});
}
});
}),
__proxyState: memorize((key) => {
if (shared.includes(createdStateList, key) && shared.instanceOf(object[key], shared.FrameworkReadableState)) {
// need to tag the key that is being tracked so that it can be updated with `state.v = xxx`.
referingObject.trackedKeys[key] = shared.trueValue;
return object[key];
}
return nestedProxyState(key);
}),
/**
* send and wait for responding with `await`
* this is always used in `nuxt3` and suspense in vue3
* @example
* ```js
* const { loading, data, error } = await useRequest(...);
* ```
*/
then(onfulfilled, onrejected) {
// open all the states to track.
shared.forEach(stateKeys, key => {
referingObject.trackedKeys[key] = shared.trueValue;
});
const handleFullfilled = () => {
// eslint-disable-next-line
shared.deleteAttr(completedProvider, 'then');
// eslint-disable-next-line
onfulfilled(completedProvider);
};
shared.isFn(providerThen) ? providerThen(handleFullfilled, onrejected) : handleFullfilled();
}
};
const completedProvider = shared.objAssign(provider, extraProvider);
return completedProvider;
},
/**
* transform state proxies to object.
* @param states proxy array of framework states
* @param filterKey filter key of state proxy
* @returns an object that contains the states of target form
*/
objectify: (states, filterKey) => states.reduce((result, item) => {
result[item.k] = filterKey ? item[filterKey] : item;
return result;
}, {}),
transformState2Proxy: (state, key) => shared.newInstance((shared.FrameworkState), state, key, state => dehydrate(state, key, referingObject), exportState, (state, newValue) => update(newValue, state, key))
};
}
const coreAssert = shared.createAssert('');
const requestHookAssert = shared.createAssert('useRequest');
const watcherHookAssert = shared.createAssert('useWatcher');
const fetcherHookAssert = shared.createAssert('useFetcher');
const coreHookAssert = (hookType) => ({
[EnumHookType.USE_REQUEST]: requestHookAssert,
[EnumHookType.USE_WATCHER]: watcherHookAssert,
[EnumHookType.USE_FETCHER]: fetcherHookAssert
})[hookType];
/**
* Assert whether it is a method instance
* @param methodInstance method instance
*/
const assertMethod = (assert, methodInstance) => assert(shared.instanceOf(methodInstance, alova.Method), 'expected a method instance.');
const KEY_SUCCESS = 'success';
const KEY_ERROR = 'error';
const KEY_COMPLETE = 'complete';
var createHook = (ht, c, eventManager, ro) => ({
/** The method instance of the last request */
m: shared.undefinedValue,
/** sent method keys */
rf: {},
/** frontStates */
fs: {},
/** eventManager */
em: eventManager,
/** hookType, useRequest=1, useWatcher=2, useFetcher=3 */
ht,
/** hook config */
c,
/** referingObject */
ro,
/** merged states */
ms: {}
});
// base event
class AlovaEventBase {
constructor(method, args) {
this.method = method;
this.args = args;
}
clone() {
return { ...this };
}
static spawn(method, args) {
return shared.newInstance((AlovaEventBase), method, args);
}
}
class AlovaSuccessEvent extends AlovaEventBase {
constructor(base, data, fromCache) {
super(base.method, base.args);
this.data = data;
this.fromCache = fromCache;
}
}
class AlovaErrorEvent extends AlovaEventBase {
constructor(base, error) {
super(base.method, base.args);
this.error = error;
}
}
class AlovaCompleteEvent extends AlovaEventBase {
constructor(base, status, data, fromCache, error) {
super(base.method, base.args);
this.status = status;
this.data = data;
this.fromCache = status === 'error' ? false : fromCache;
this.error = error;
}
}
/** Sq top level events */
class SQEvent {
constructor(behavior, method, silentMethod) {
this.behavior = behavior;
this.method = method;
this.silentMethod = silentMethod;
}
}
/** Sq global events */
class GlobalSQEvent extends SQEvent {
constructor(behavior, method, silentMethod, queueName, retryTimes) {
super(behavior, method, silentMethod);
this.queueName = queueName;
this.retryTimes = retryTimes;
}
}
class GlobalSQSuccessEvent extends GlobalSQEvent {
constructor(behavior, method, silentMethod, queueName, retryTimes, data, vDataResponse) {
super(behavior, method, silentMethod, queueName, retryTimes);
this.data = data;
this.vDataResponse = vDataResponse;
}
}
class GlobalSQErrorEvent extends GlobalSQEvent {
constructor(behavior, method, silentMethod, queueName, retryTimes, error, retryDelay) {
super(behavior, method, silentMethod, queueName, retryTimes);
this.error = error;
this.retryDelay = retryDelay;
}
}
class GlobalSQFailEvent extends GlobalSQEvent {
constructor(behavior, method, silentMethod, queueName, retryTimes, error) {
super(behavior, method, silentMethod, queueName, retryTimes);
this.error = error;
}
}
/** Sq event */
class ScopedSQEvent extends SQEvent {
constructor(behavior, method, silentMethod, args) {
super(behavior, method, silentMethod);
this.args = args;
}
}
class ScopedSQSuccessEvent extends ScopedSQEvent {
constructor(behavior, method, silentMethod, args, data) {
super(behavior, method, silentMethod, args);
this.data = data;
}
}
class ScopedSQErrorEvent extends ScopedSQEvent {
constructor(behavior, method, silentMethod, args, error) {
super(behavior, method, silentMethod, args);
this.error = error;
}
}
class ScopedSQRetryEvent extends ScopedSQEvent {
constructor(behavior, method, silentMethod, args, retryTimes, retryDelay) {
super(behavior, method, silentMethod, args);
this.retryTimes = retryTimes;
this.retryDelay = retryDelay;
}
}
class ScopedSQCompleteEvent extends ScopedSQEvent {
constructor(behavior, method, silentMethod, args, status, data, error) {
super(behavior, method, silentMethod, args);
this.status = status;
this.data = data;
this.error = error;
}
}
class RetriableRetryEvent extends AlovaEventBase {
constructor(base, retryTimes, retryDelay) {
super(base.method, base.args);
this.retryTimes = retryTimes;
this.retryDelay = retryDelay;
}
}
class RetriableFailEvent extends AlovaErrorEvent {
constructor(base, error, retryTimes) {
super(base, error);
this.retryTimes = retryTimes;
}
}
const defaultMiddleware = (_, next) => next();
const stateCache = {};
/**
* @description Get State cache data
* @param baseURL Base URL
* @param key Request key value
* @returns Cached response data, if not returned {}
*/
const getStateCache = (namespace, key) => {
const cachedState = stateCache[namespace] || {};
return cachedState[key] ? Array.from(cachedState[key]) : [];
};
/**
* @description Set State cache data
* @param baseURL Base URL
* @param key Request key value
* @param data cache data
*/
const setStateCache = (namespace, key, hookInstance) => {
const cachedState = (stateCache[namespace] = stateCache[namespace] || {});
if (!cachedState[key]) {
cachedState[key] = shared.newInstance((Set));
}
cachedState[key].add(hookInstance);
};
/**
* @description Clear State cache data
* @param baseURL Base URL
* @param key Request key value
*/
const removeStateCache = (namespace, key, hookInstance) => {
const cachedState = stateCache[namespace];
const hookSet = cachedState[key];
if (cachedState && hookSet) {
hookInstance ? hookSet.delete(hookInstance) : hookSet.clear();
if (hookSet.size === 0) {
shared.deleteAttr(cachedState, key);
}
}
};
/**
* Unified processing of request logic for useRequest/useWatcher/useFetcher and other request hook functions
* @param hookInstance hook instance
* @param methodHandler Request method object or get function
* @param sendCallingArgs send function parameters
* @returns Request status
*/
function useHookToSendRequest(hookInstance, methodHandler, sendCallingArgs = []) {
const currentHookAssert = coreHookAssert(hookInstance.ht);
let methodInstance = shared.getHandlerMethod(methodHandler, currentHookAssert, sendCallingArgs);
const { fs: frontStates, ht: hookType, c: useHookConfig } = hookInstance;
const { loading: loadingState, data: dataState, error: errorState } = frontStates;
const isFetcher = hookType === EnumHookType.USE_FETCHER;
const { force: forceRequest = shared.falseValue, middleware = defaultMiddleware } = useHookConfig;
const alovaInstance = shared.getContext(methodInstance);
const { id } = alovaInstance;
// If it is a silent request, on success will be called directly after the request, on error will not be triggered, and progress will not be updated.
const methodKey = shared.getMethodInternalKey(methodInstance);
const { abortLast = shared.trueValue } = useHookConfig;
const isFirstRequest = !hookInstance.m;
hookInstance.m = methodInstance;
return (async () => {
// Initialize status data, which does not need to be loaded when pulling data, because pulling data does not require returning data.
let removeStates = shared.noop;
let isNextCalled = shared.falseValue;
let responseHandlePromise = shared.promiseResolve(shared.undefinedValue);
let offDownloadEvent = shared.noop;
let offUploadEvent = shared.noop;
const cachedResponse = await alova.queryCache(methodInstance);
let fromCache = () => !!cachedResponse;
// Whether it is a controlled loading state. When it is true, loading will no longer be set to false in response processing.
let controlledLoading = shared.falseValue;
if (!isFetcher) {
// Store the initial state in cache for subsequent updates
setStateCache(id, methodKey, hookInstance);
// Setting the state removal function will be passed to the effect request in the hook, and it will be set to be called when the component is unloaded.
removeStates = () => removeStateCache(id, methodKey, hookInstance);
}
// The middleware function next callback function allows you to modify mandatory request parameters and even replace the method instance that is about to send the request.
const guardNext = guardNextConfig => {
isNextCalled = shared.trueValue;
const { force: guardNextForceRequest = forceRequest, method: guardNextReplacingMethod = methodInstance } = guardNextConfig || {};
const forceRequestFinally = shared.sloughConfig(guardNextForceRequest, [
shared.newInstance((AlovaEventBase), methodInstance, sendCallingArgs)
]);
const progressUpdater = (stage) => ({ loaded, total }) => {
frontStates[stage].v = {
loaded,
total
};
};
methodInstance = guardNextReplacingMethod;
// The latest controller needs to be saved every time a request is sent
hookInstance.rf[methodKey] = removeStates;
// Loading will not be changed when the loading state is controlled
// The cache is missed, or loading needs to be set to true when forcing a request.
if (!controlledLoading) {
loadingState.v = !!forceRequestFinally || !cachedResponse;
}
// Determine whether to trigger a progress update based on the tracking status of downloading and uploading
const { downloading: enableDownload, uploading: enableUpload } = hookInstance.ro.trackedKeys;
offDownloadEvent = enableDownload ? methodInstance.onDownload(progressUpdater('downloading')) : offDownloadEvent;
offUploadEvent = enableUpload ? methodInstance.onUpload(progressUpdater('uploading')) : offUploadEvent;
responseHandlePromise = methodInstance.send(forceRequestFinally);
fromCache = () => methodInstance.fromCache || shared.falseValue;
return responseHandlePromise;
};
// Call middleware function
const commonContext = {
method: methodInstance,
cachedResponse,
config: useHookConfig,
abort: () => methodInstance.abort()
};
// Whether it is necessary to update the response data and call the response callback
const toUpdateResponse = () => hookType !== EnumHookType.USE_WATCHER || !abortLast || hookInstance.m === methodInstance;
const controlLoading = (control = shared.trueValue) => {
// only reset loading state in first request
if (control && isFirstRequest) {
loadingState.v = shared.falseValue;
}
controlledLoading = control;
};
// Call middleware function
const middlewareCompletePromise = isFetcher
? middleware({
...commonContext,
args: sendCallingArgs,
fetch: (methodInstance, ...args) => {
assertMethod(currentHookAssert, methodInstance);
return useHookToSendRequest(hookInstance, methodInstance, args);
},
proxyStates: shared.omit(frontStates, 'data'),
controlLoading
}, guardNext)
: middleware({
...commonContext,
args: sendCallingArgs,
send: (...args) => useHookToSendRequest(hookInstance, methodHandler, args),
proxyStates: frontStates,
controlLoading
}, guardNext);
let finallyResponse = shared.undefinedValue;
const baseEvent = (AlovaEventBase).spawn(methodInstance, sendCallingArgs);
try {
// Unified processing of responses
const middlewareReturnedData = await middlewareCompletePromise;
const afterSuccess = (data) => {
// Update cached response data
if (!isFetcher) {
toUpdateResponse() && (dataState.v = data);
}
else if (hookInstance.c.updateState !== shared.falseValue) {
// Update the status in the cache, usually entered in use fetcher
shared.forEach(getStateCache(id, methodKey), hookInstance => {
hookInstance.fs.data.v = data;
});
}
// If the response data needs to be updated, the corresponding callback function is triggered after the request.
if (toUpdateResponse()) {
errorState.v = shared.undefinedValue;
// Loading status will no longer change to false when controlled
!controlledLoading && (loadingState.v = shared.falseValue);
hookInstance.em.emit(KEY_SUCCESS, shared.newInstance((AlovaSuccessEvent), baseEvent, data, fromCache()));
hookInstance.em.emit(KEY_COMPLETE, shared.newInstance((AlovaCompleteEvent), baseEvent, KEY_SUCCESS, data, fromCache(), shared.undefinedValue));
}
return data;
};
finallyResponse =
// When no data is returned or undefined is returned in the middleware, get the real response data
// Otherwise, use the returned data and no longer wait for the response promise. At this time, you also need to call the response callback.
middlewareReturnedData !== shared.undefinedValue
? afterSuccess(middlewareReturnedData)
: isNextCalled
? // There are two possibilities when middlewareCompletePromise is resolve
// 1. The request is normal
// 2. The request is incorrect, but the error is captured by the middleware function. At this time, the success callback will also be called, that is, afterSuccess(undefinedValue)
await shared.promiseThen(responseHandlePromise, afterSuccess, () => afterSuccess(shared.undefinedValue))
: // If is next called is not called, no data is returned
shared.undefinedValue;
// When the next function is not called, update loading to false.
!isNextCalled && !controlledLoading && (loadingState.v = shared.falseValue);
}
catch (error) {
if (toUpdateResponse()) {
// Controls the output of error messages
errorState.v = error;
// Loading status will no longer change to false when controlled
!controlledLoading && (loadingState.v = shared.falseValue);
hookInstance.em.emit(KEY_ERROR, shared.newInstance((AlovaErrorEvent), baseEvent, error));
hookInstance.em.emit(KEY_COMPLETE, shared.newInstance((AlovaCompleteEvent), baseEvent, KEY_ERROR, shared.undefinedValue, fromCache(), error));
}
throw error;
}
// Unbind download and upload events after responding
offDownloadEvent();
offUploadEvent();
return finallyResponse;
})();
}
const refCurrent = (ref) => ref.current;
/**
* Create request status and uniformly process consistent logic in useRequest, useWatcher, and useFetcher
* This function will call the creation function of statesHook to create the corresponding request state.
* When the value is empty, it means useFetcher enters, and data status and cache status are not needed at this time.
* @param methodInstance request method object
* @param useHookConfig hook request configuration object
* @param initialData Initial data data
* @param immediate Whether to initiate a request immediately
* @param watchingStates The monitored status, if not passed in, call handleRequest directly.
* @param debounceDelay Delay time for request initiation
* @returns Current request status, operation function and event binding function
*/
function createRequestState(hookType, methodHandler, useHookConfig, initialData, immediate = shared.falseValue, watchingStates, debounceDelay = 0) {
var _a;
// shallow clone config object to avoid passing the same useHookConfig object which may cause vue2 state update error
useHookConfig = { ...useHookConfig };
let initialLoading = !!immediate;
let cachedResponse = shared.undefinedValue;
// When sending a request immediately, you need to determine the initial loading value by whether to force the request and whether there is a cache. This has the following two benefits:
// 1. Sending the request immediately under react can save one rendering time
// 2. In the HTML rendered by SSR, the initial view is in the loading state to avoid the loading view flashing when displayed on the client.
if (immediate) {
// An error may be reported when calling the get handler method, and try/catch is required.
try {
const methodInstance = shared.getHandlerMethod(methodHandler, coreHookAssert(hookType));
const alovaInstance = shared.getContext(methodInstance);
const l1CacheResult = alovaInstance.l1Cache.get(shared.buildNamespacedCacheKey(alovaInstance.id, shared.getMethodInternalKey(methodInstance)));
// The cache is only checked synchronously, so it does not take effect on asynchronous l1Cache adapters.
// It is recommended not to set up the asynchronous l1Cache adapter on the client side
if (l1CacheResult && !shared.instanceOf(l1CacheResult, shared.PromiseCls)) {
const [data, expireTimestamp] = l1CacheResult;
// If there is no expiration time, it means that the data will never expire. Otherwise, you need to determine whether it has expired.
if (!expireTimestamp || expireTimestamp > shared.getTime()) {
cachedResponse = data;
}
}
const forceRequestFinally = shared.sloughConfig((_a = useHookConfig.force) !== null && _a !== void 0 ? _a : shared.falseValue);
initialLoading = !!forceRequestFinally || !cachedResponse;
}
catch (_b) { }
}
const { create, effectRequest, ref, objectify, exposeProvider, transformState2Proxy, __referingObj: referingObject } = statesHookHelper(alova.promiseStatesHook(), useHookConfig.__referingObj);
const progress = {
total: 0,
loaded: 0
};
// Put the externally incoming supervised states into the front states collection together
const { managedStates = {} } = useHookConfig;
const managedStatesProxy = mapObject(managedStates, (state, key) => transformState2Proxy(state, key));
const data = create(cachedResponse !== null && cachedResponse !== void 0 ? cachedResponse : (shared.isFn(initialData) ? initialData() : initialData), 'data');
const loading = create(initialLoading, 'loading');
const error = create(shared.undefinedValue, 'error');
const downloading = create({ ...progress }, 'downloading');
const uploading = create({ ...progress }, 'uploading');
const frontStates = objectify([data, loading, error, downloading, uploading]);
const eventManager = shared.createEventManager();
const hookInstance = refCurrent(ref(createHook(hookType, useHookConfig, eventManager, referingObject)));
/**
* ## react
* Every time the function is executed, the following items need to be reset
*/
hookInstance.fs = frontStates;
hookInstance.em = eventManager;
hookInstance.c = useHookConfig;
hookInstance.ms = { ...frontStates, ...managedStatesProxy };
const hasWatchingStates = watchingStates !== shared.undefinedValue;
// Initialize request event
// Unified send request function
const handleRequest = (handler = methodHandler, sendCallingArgs) => useHookToSendRequest(hookInstance, handler, sendCallingArgs);
// if user call hook like `await useRequest(...)`
// that will stop the immediate request, because it will be call a request in function `then`
const hookRequestPromiseCallback = ref(shared.undefinedValue);
const isInitialRequest = ref(shared.falseValue);
// only call once when multiple values changed at the same time
const onceRunner = refCurrent(ref(shared.createSyncOnceRunner()));
// Call `handleRequest` in a way that catches the exception
// Catching exceptions prevents exceptions from being thrown out
const wrapEffectRequest = (ro = referingObject, handler) => {
onceRunner(() => {
// Do not send requests when rendering on the server side
// but if call hook with `await`, the `hookRequestPromiseCallback` will be set as `resolve` and `reject` function
if (!alova.globalConfigMap.ssr || refCurrent(hookRequestPromiseCallback)) {
// `referingObject.initialRequest` is used in nuxthook
referingObject.initialRequest = isInitialRequest.current = shared.trueValue;
shared.promiseThen(handleRequest(handler), () => {
var _a;
(_a = refCurrent(hookRequestPromiseCallback)) === null || _a === void 0 ? void 0 : _a.resolve();
}, error => {
var _a;
// the error tracking indicates that the error need to throw.
// when user access the `error` state or bind the error event, the error instance won't be thrown out.
if (!ro.bindError && !ro.trackedKeys.error && !refCurrent(hookRequestPromiseCallback)) {
throw error;
}
(_a = refCurrent(hookRequestPromiseCallback)) === null || _a === void 0 ? void 0 : _a.reject(error);
});
}
});
};
/**
* fix: https://github.com/alovajs/alova/issues/421
* Use ref wraps to prevent react from creating new debounce function in every render
* Explicit passing is required because the context will change
*/
const debouncingSendHandler = ref(debounce((_, ro, handler) => wrapEffectRequest(ro, handler), (changedIndex) => shared.isNumber(changedIndex) ? (shared.isArray(debounceDelay) ? debounceDelay[changedIndex] : debounceDelay) : 0));
effectRequest({
handler:
// When `watchingStates` is an array, it indicates the watching states (including an empty array). When it is undefined, it indicates the non-watching state.
hasWatchingStates
? (changedIndex) => debouncingSendHandler.current(changedIndex, referingObject, methodHandler)
: () => wrapEffectRequest(referingObject),
removeStates: () => {
shared.forEach(shared.objectValues(hookInstance.rf), fn => fn());
},
frontStates: { ...frontStates, ...managedStatesProxy },
watchingStates,
immediate: immediate !== null && immediate !== void 0 ? immediate : shared.trueValue
});
const hookProvider = exposeProvider({
...objectify([data, loading, error, downloading, uploading]),
abort: () => hookInstance.m && hookInstance.m.abort(),
/**
* Manually initiate a request by executing this method
* @param sendCallingArgs Parameters passed in when calling the send function
* @param methodInstance method object
* @param isFetcher Whether to call isFetcher
* @returns Request promise
*/
send: (sendCallingArgs, methodInstance) => handleRequest(methodInstance, sendCallingArgs),
onSuccess(handler) {
eventManager.on(KEY_SUCCESS, handler);
},
onError(handler) {
// will not throw error when bindError is true.
// it will reset in `exposeProvider` so that ignore the error binding in custom use hooks.
referingObject.bindError = shared.trueValue;
eventManager.on(KEY_ERROR, handler);
},
onComplete(handler) {
eventManager.on(KEY_COMPLETE, handler);
},
/**
* send and wait for responding with `await`
* this is always used in `nuxt3` and `<suspense>` in vue3
* @example
* ```js
* const { loading, data, error } = await useRequest(...);
* ```
*/
then(onfulfilled, onrejected) {
const { promise, resolve, reject } = shared.usePromise();
hookRequestPromiseCallback.current = {
resolve,
reject
};
// if the request handler is not called, the promise will resolve asynchronously.
shared.setTimeoutFn(() => {
!isInitialRequest.current && resolve();
}, 10);
shared.promiseThen(promise, () => {
onfulfilled(hookProvider);
}, onrejected);
}
});
return hookProvider;
}
/**
* Fetch request data and cache request method object
*/
function useFetcher(config = {}) {
const props = createRequestState(EnumHookType.USE_FETCHER, shared.noop, config);
const { send } = props;
shared.deleteAttr(props, 'send');
return shared.objAssign(props, {
/**
* Fetch data fetch will definitely send a request, and if the currently requested data has a corresponding management state, this state will be updated.
* @param matcher Method object
*/
fetch: (matcher, ...args) => {
assertMethod(fetcherHookAssert, matcher);