• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alovajs / alova / #259

14 Dec 2025 05:27AM UTC coverage: 95.022% (+0.007%) from 95.015%
#259

push

github

web-flow
ci: release (#782)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

2064 of 2197 branches covered (93.95%)

Branch coverage included in aggregate %.

7061 of 7406 relevant lines covered (95.34%)

246.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.87
/packages/shared/src/function.ts
1
import type { CacheExpire, CacheMode, MethodRequestConfig } from '../../alova/typings';
1✔
2
import type { BackoffPolicy, GeneralFn, UsePromiseExposure } from './types';
3
import {
4
  JSONStringify,
5
  MEMORY,
6
  ObjectCls,
7
  PromiseCls,
8
  STORAGE_RESTORE,
9
  falseValue,
10
  filterItem,
11
  forEach,
12
  isArray,
13
  len,
14
  mapItem,
15
  nullValue,
16
  objectKeys,
17
  promiseThen,
18
  pushItem,
19
  setTimeoutFn,
20
  shift,
21
  trueValue,
22
  typeOf,
23
  undefinedValue
24
} from './vars';
25

26
/**
27
 * Empty function for compatibility processing
28
 */
29
export const noop = () => {};
1✔
30
/**
31
 * A function that returns the parameter itself, used for compatibility processing
32
 * Since some systems use self as a reserved word, $self is used to distinguish it.
33
 * @param arg any parameter
34
 * @returns return parameter itself
35
 */
36
export const $self = <T>(arg: T) => arg;
1✔
37
/**
38
 * Determine whether the parameter is a function any parameter
39
 * @returns Whether the parameter is a function
40
 */
41
export const isFn = (arg: any): arg is GeneralFn => typeOf(arg) === 'function';
1✔
42
/**
43
 * Determine whether the parameter is a number any parameter
44
 * @returns Whether the parameter is a number
45
 */
46
export const isNumber = (arg: any): arg is number => typeOf(arg) === 'number' && !Number.isNaN(arg);
1✔
47
/**
48
 * Determine whether the parameter is a string any parameter
49
 * @returns Whether the parameter is a string
50
 */
51
export const isString = (arg: any): arg is string => typeOf(arg) === 'string';
1✔
52
/**
53
 * Determine whether the parameter is an object any parameter
54
 * @returns Whether the parameter is an object
55
 */
56
export const isObject = (arg: any): arg is Record<any, unknown> => arg !== nullValue && typeOf(arg) === 'object';
1✔
57
/**
58
 * Global toString any parameter stringified parameters
59
 */
60
export const globalToString = (arg: any) => ObjectCls.prototype.toString.call(arg);
1✔
61
/**
62
 * Determine whether it is a normal object any parameter
63
 * @returns Judgment result
64
 */
65
export const isPlainObject = (arg: any): arg is Record<any, any> => globalToString(arg) === '[object Object]';
1✔
66
/**
67
 * Determine whether it is an instance of a certain class any parameter
68
 * @returns Judgment result
69
 */
70
export const instanceOf = <T>(arg: any, cls: new (...args: any[]) => T): arg is T => arg instanceof cls;
1✔
71
/**
72
 * Unified timestamp acquisition function
73
 * @returns Timestamp
74
 */
75
export const getTime = (date?: Date) => (date ? date.getTime() : Date.now());
1✔
76
/**
77
 * Get the alova instance through the method instance alova example
78
 */
79
export const getContext = <T extends { context: any }>(methodInstance: T): T['context'] => methodInstance.context;
1✔
80
/**
81
 * Get method instance configuration data
82
 * @returns Configuration object
83
 */
84
export const getConfig = <T extends { config: any }>(methodInstance: T): T['config'] => methodInstance.config;
1✔
85
/**
86
 * Get alova configuration data alova configuration object
87
 */
88
export const getContextOptions = <T extends { options: any }>(alovaInstance: T): T['options'] => alovaInstance.options;
1✔
89
/**
90
 * Get alova configuration data through method instance alova configuration object
91
 */
92
export const getOptions = <T extends { context: any }>(methodInstance: T) =>
1✔
93
  getContextOptions(getContext(methodInstance));
×
94

95
/**
96
 * Get the key value of the request method
97
 * @returns The key value of this request method
98
 */
99
export const key = <
1✔
100
  T extends {
101
    config: any;
102
    type: string;
103
    url: string;
104
    data?: any;
105
  }
106
>(
107
  methodInstance: T
1✔
108
) => {
1✔
109
  const { params, headers } = getConfig(methodInstance);
1✔
110
  return JSONStringify([methodInstance.type, methodInstance.url, params, methodInstance.data, headers]);
1✔
111
};
1✔
112

113
/**
114
 * Create uuid simple version uuid
115
 */
116
export const uuid = () => {
1✔
117
  const timestamp = new Date().getTime();
40,001✔
118
  return Math.floor(Math.random() * timestamp).toString(36);
40,001✔
119
};
40,001✔
120

121
/**
122
 * Get the key value of the method instance method instance
123
 * @returns The key value of this method instance
124
 */
125
export const getMethodInternalKey = <T extends { key: string }>(methodInstance: T): T['key'] => methodInstance.key;
1✔
126

127
/**
128
 * Get the request method object
129
 * @param methodHandler Request method handle
130
 * @param args Method call parameters request method object
131
 */
132
export const getHandlerMethod = <T extends { key: string }>(
1✔
133
  methodHandler: T | ((...args: any[]) => T),
4✔
134
  assert: (expression: boolean, msg: string) => void,
4✔
135
  args: any[] = []
4✔
136
) => {
4✔
137
  const methodInstance = isFn(methodHandler) ? methodHandler(...args) : methodHandler;
4✔
138
  assert(!!methodInstance.key, 'hook handler must be a method instance or a function that returns method instance');
4✔
139
  return methodInstance;
4✔
140
};
4✔
141
/**
142
 * Is it special data
143
 * @param data Submit data
144
 * @returns Judgment result
145
 */
146
export const isSpecialRequestBody = (data: any) => {
1✔
147
  const dataTypeString = globalToString(data);
10✔
148
  return (
10✔
149
    /^\[object (Blob|FormData|ReadableStream|URLSearchParams)\]$/i.test(dataTypeString) || instanceOf(data, ArrayBuffer)
10✔
150
  );
151
};
10✔
152

153
export const objAssign = <T extends Record<string, any>, U extends Record<string, any>[]>(
1✔
154
  target: T,
×
155
  ...sources: U
×
156
): T & U[number] => ObjectCls.assign(target, ...sources);
×
157

158
// Write an omit function type
159
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
160

161
/**
162
 * Excludes specified attributes from a data collection and returns a new data collection data collection
163
 * @param keys Excluded keys new data collection
164
 */
165
export const omit = <T extends Record<string, any>, K extends keyof T>(obj: T, ...keys: K[]) => {
1✔
166
  const result = {} as Pick<T, Exclude<keyof T, K>>;
5✔
167
  for (const key in obj) {
5✔
168
    if (!keys.includes(key as any)) {
13✔
169
      (result as any)[key] = obj[key];
7✔
170
    }
7✔
171
  }
13✔
172
  return result;
5✔
173
};
5✔
174

175
/**
176
 * the same as `Promise.withResolvers`
177
 * @returns promise with resolvers.
178
 */
179
export function usePromise<T = any>(): UsePromiseExposure<T> {
1✔
180
  let retResolve: UsePromiseExposure<T>['resolve'];
×
181
  let retReject: UsePromiseExposure<T>['reject'];
×
182
  const promise = new Promise<T>((resolve, reject) => {
×
183
    retResolve = resolve;
×
184
    retReject = reject;
×
185
  });
×
186

187
  return { promise, resolve: retResolve!, reject: retReject! };
×
188
}
×
189

190
/**
191
 * Get cached configuration parameters, fixedly returning an object in the format { e: function, c: any, f: any, m: number, s: boolean, t: string } e is the abbreviation of expire, which returns the cache expiration time point (timestamp) in milliseconds.
192
 * c is controlled, indicating whether it is a controlled cache
193
 * f is the original value of cacheFor, which is used to call to obtain cached data when c is true.
194
 * m is the abbreviation of mode, storage mode
195
 * s is the abbreviation of storage, whether to store it locally
196
 * t is the abbreviation of tag, which stores tags persistently.
197
 * @param methodInstance method instance
198
 * @returns Unified cache parameter object
199
 */
200
export const getLocalCacheConfigParam = <T extends { config: any }>(methodInstance: T) => {
1✔
201
  const { cacheFor } = getConfig(methodInstance);
6✔
202

203
  const getCacheExpireTs = (cacheExpire: CacheExpire) =>
6✔
204
    isNumber(cacheExpire) ? getTime() + cacheExpire : getTime(cacheExpire || undefinedValue);
7!
205
  let cacheMode: CacheMode = MEMORY;
6✔
206
  let expire: (mode: CacheMode) => ReturnType<typeof getCacheExpireTs> = () => 0;
6✔
207
  let store = falseValue;
6✔
208
  let tag: undefined | string = undefinedValue;
6✔
209
  const controlled = isFn(cacheFor);
6✔
210
  if (!controlled) {
6✔
211
    let expireColumn: any = cacheFor;
5✔
212
    if (isPlainObject(cacheFor)) {
5✔
213
      const { mode = MEMORY, expire, tag: configTag } = (cacheFor as Exclude<typeof cacheFor, Date>) || {};
2!
214
      cacheMode = mode;
2✔
215
      store = mode === STORAGE_RESTORE;
2✔
216
      tag = configTag ? configTag.toString() : undefinedValue;
2✔
217
      expireColumn = expire;
2✔
218
    }
2✔
219
    expire = (mode: CacheMode) =>
5✔
220
      getCacheExpireTs(isFn(expireColumn) ? expireColumn({ method: methodInstance, mode }) : expireColumn);
7✔
221
  }
5✔
222
  return {
6✔
223
    f: cacheFor,
6✔
224
    c: controlled,
6✔
225
    e: expire,
6✔
226
    m: cacheMode,
6✔
227
    s: store,
6✔
228
    t: tag
6✔
229
  };
6✔
230
};
6✔
231

232
/**
233
 * Create class instance
234
 * @param Cls Constructor
235
 * @param args Constructor parameters class instance
236
 */
237
export const newInstance = <T extends { new (...args: any[]): InstanceType<T> }>(
1✔
238
  Cls: T,
14✔
239
  ...args: ConstructorParameters<T>
14✔
240
) => new Cls(...args);
14✔
241
/**
242
 * Unified configuration
243
 * @param data
244
 * @returns unified configuration
245
 */
246
export const sloughConfig = <T>(config: T | ((...args: any[]) => T), args: any[] = []) =>
1✔
247
  isFn(config) ? config(...args) : config;
×
248
export const sloughFunction = <T, U>(arg: T | undefined, defaultFn: U) =>
1✔
249
  isFn(arg) ? arg : ![falseValue, nullValue].includes(arg as any) ? defaultFn : noop;
×
250

251
/**
252
 * Create an executor that calls multiple times synchronously and only executes it once asynchronously
253
 */
254
export const createSyncOnceRunner = (delay = 0) => {
1✔
255
  let timer: NodeJS.Timeout | number | undefined = undefinedValue;
1✔
256

257
  // Executing multiple calls to this function will execute once asynchronously
258
  return (fn: () => void) => {
1✔
259
    if (timer) {
3✔
260
      clearTimeout(timer);
2✔
261
    }
2✔
262
    timer = setTimeoutFn(fn, delay);
3✔
263
  };
3✔
264
};
1✔
265

266
/**
267
 * Create an asynchronous function queue, the asynchronous function will be executed serially queue add function
268
 */
269
export const createAsyncQueue = (catchError = falseValue) => {
1✔
270
  type AsyncFunction<T = any> = (...args: any[]) => Promise<T>;
271
  const queue: AsyncFunction[] = [];
4✔
272
  let completedHandler: GeneralFn | undefined = undefinedValue;
4✔
273
  let executing = false;
4✔
274

275
  const executeQueue = async () => {
4✔
276
    executing = trueValue;
4✔
277
    while (len(queue) > 0) {
4✔
278
      const asyncFunc = shift(queue);
7✔
279
      if (asyncFunc) {
7✔
280
        await asyncFunc();
7✔
281
      }
7✔
282
    }
7✔
283
    completedHandler && completedHandler();
4!
284
    executing = falseValue;
4✔
285
  };
4✔
286
  const addQueue = <T>(asyncFunc: AsyncFunction<T>): Promise<T> =>
4✔
287
    newInstance(PromiseCls<T>, (resolve, reject) => {
7✔
288
      const wrappedFunc = () =>
7✔
289
        promiseThen(asyncFunc(), resolve, err => {
7✔
290
          catchError ? resolve(undefinedValue as T) : reject(err);
2✔
291
        });
7✔
292
      pushItem(queue, wrappedFunc);
7✔
293
      if (!executing) {
7✔
294
        executeQueue();
4✔
295
      }
4✔
296
    });
7✔
297

298
  const onComplete = (fn: GeneralFn) => {
4✔
299
    completedHandler = fn;
×
300
  };
×
301

302
  return {
4✔
303
    addQueue,
4✔
304
    onComplete
4✔
305
  };
4✔
306
};
4✔
307

308
/**
309
 * Traverse the target object deeply target audience
310
 * @param callback Traversal callback
311
 * @param preorder Whether to traverse in preorder, the default is true
312
 * @param key The currently traversed key
313
 * @param parent The parent node currently traversed
314
 */
315
export const walkObject = (
1✔
316
  target: any,
16✔
317
  callback: (value: any, key: string | number | symbol, parent: any) => void,
16✔
318
  preorder = trueValue,
16✔
319
  key?: string | number | symbol,
16✔
320
  parent?: any
16✔
321
) => {
16✔
322
  const callCallback = () => {
16✔
323
    if (parent && key) {
16✔
324
      target = callback(target, key, parent);
13✔
325
      if (target !== parent[key]) {
13✔
326
        parent[key] = target;
1✔
327
      }
1✔
328
    }
13✔
329
  };
16✔
330

331
  // Preorder traversal
332
  preorder && callCallback();
16✔
333
  if (isObject(target)) {
16✔
334
    for (const i in target) {
6✔
335
      if (!instanceOf(target, String)) {
13✔
336
        walkObject(target[i], callback, preorder, i, target);
13✔
337
      }
13✔
338
    }
13✔
339
  }
6✔
340
  // Postal order traversal
341
  !preorder && callCallback();
16✔
342
  return target;
16✔
343
};
16✔
344

345
const cacheKeyPrefix = '$a.';
1✔
346
/**
347
 * build common cache key.
348
 */
349
export const buildNamespacedCacheKey = (namespace: string, key: string) => cacheKeyPrefix + namespace + key;
1✔
350

351
/**
352
 * Determine whether it is a cache key built by alova
353
 * @param key Cache key
354
 */
355
export const isAlovaCacheKey = (key: string) => key.startsWith(cacheKeyPrefix);
1✔
356

357
/**
358
 * Calculate retry delay time based on avoidance strategy and number of retries avoid parameters
359
 * @param retryTimes Number of retries
360
 * @returns Retry delay time
361
 */
362
export const delayWithBackoff = (backoff: BackoffPolicy, retryTimes: number) => {
1✔
363
  let { startQuiver, endQuiver } = backoff;
×
364
  const { delay, multiplier = 1 } = backoff;
×
365
  let retryDelayFinally = (delay || 0) * multiplier ** (retryTimes - 1);
×
366
  // If start quiver or end quiver has a value, you need to increase the random jitter value in the specified range
367
  if (startQuiver || endQuiver) {
×
368
    startQuiver = startQuiver || 0;
×
369
    endQuiver = endQuiver || 1;
×
370
    retryDelayFinally +=
×
371
      retryDelayFinally * startQuiver + Math.random() * retryDelayFinally * (endQuiver - startQuiver);
×
372
    retryDelayFinally = Math.floor(retryDelayFinally); // round delay
×
373
  }
×
374
  return retryDelayFinally;
×
375
};
×
376

377
/**
378
 * Build the complete url baseURL path url parameters complete url
379
 */
380
export const buildCompletedURL = (baseURL: string, url: string, params: MethodRequestConfig['params']) => {
1✔
381
  // Check if the URL starts with http/https
382
  const startsWithPrefix = /^https?:\/\//i.test(url);
16✔
383

384
  if (!startsWithPrefix) {
16✔
385
    // If the Base url ends with /, remove /
386
    baseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
13✔
387
    // If it does not start with /or http protocol, you need to add /
388

389
    // Compatible with some RESTful usage fix: https://github.com/alovajs/alova/issues/382
390
    if (url !== '') {
13✔
391
      // Since absolute URLs (http/https) are handled above,
392
      // we only need to ensure relative URLs start with a forward slash
393
      url = url.startsWith('/') ? url : `/${url}`;
9✔
394
    }
9✔
395
  }
13✔
396

397
  // fix: https://github.com/alovajs/alova/issues/653
398
  const completeURL = startsWithPrefix ? url : baseURL + url;
16✔
399

400
  // Convert params object to get string
401
  // Filter out those whose value is undefined
402
  const paramsStr = isString(params)
16✔
403
    ? params
1✔
404
    : mapItem(
15✔
405
        filterItem(objectKeys(params), key => params[key] !== undefinedValue),
15✔
406
        key => `${key}=${params[key]}`
15✔
407
      ).join('&');
15✔
408
  // Splice the get parameters behind the url. Note that the url may already have parameters.
409
  return paramsStr
16✔
410
    ? +completeURL.includes('?')
6✔
411
      ? `${completeURL}&${paramsStr}`
2✔
412
      : `${completeURL}?${paramsStr}`
4✔
413
    : completeURL;
10✔
414
};
16✔
415

416
/**
417
 * Deep clone an object.
418
 *
419
 * @param obj The object to be cloned.
420
 * @returns The cloned object.
421
 */
422
export const deepClone = <T>(obj: T): T => {
1✔
423
  if (isArray(obj)) {
42✔
424
    return mapItem(obj, deepClone) as T;
7✔
425
  }
7✔
426

427
  if (isPlainObject(obj) && obj.constructor === ObjectCls) {
42✔
428
    const clone = {} as T;
12✔
429
    forEach(objectKeys(obj), key => {
12✔
430
      clone[key as keyof T] = deepClone(obj[key as keyof T]);
22✔
431
    });
12✔
432
    return clone;
12✔
433
  }
12✔
434
  return obj;
23✔
435
};
23✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc