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

alovajs / alova / #219

01 Nov 2024 02:50PM UTC coverage: 95.359% (+1.5%) from 93.83%
#219

push

github

web-flow
Merge pull request #577 from alovajs/changeset-release/main

ci: release

1698 of 1787 branches covered (95.02%)

Branch coverage included in aggregate %.

5801 of 6077 relevant lines covered (95.46%)

223.07 hits per line

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

97.62
/packages/client/src/hooks/silent/silentQueue.ts
1
import {
2✔
2
  GlobalSQErrorEvent,
3
  GlobalSQEvent,
4
  GlobalSQFailEvent,
5
  GlobalSQSuccessEvent,
6
  ScopedSQErrorEvent,
7
  ScopedSQRetryEvent
8
} from '@/event';
9
import updateState from '@/updateState';
10
import {
11
  PromiseCls,
12
  RegExpCls,
13
  delayWithBackoff,
14
  falseValue,
15
  forEach,
16
  instanceOf,
17
  isObject,
18
  isString,
19
  len,
20
  mapItem,
21
  newInstance,
22
  noop,
23
  objectKeys,
24
  promiseThen,
25
  pushItem,
26
  regexpTest,
27
  setTimeoutFn,
28
  shift,
29
  sloughConfig,
30
  trueValue,
31
  walkObject
32
} from '@alova/shared';
33
import { AlovaGenerics, Method, setCache } from 'alova';
34
import { RetryErrorDetailed, SilentMethod, SilentQueueMap, UpdateStateCollection } from '~/typings/clienthook';
35
import {
36
  BEHAVIOR_SILENT,
37
  BeforeEventKey,
38
  DEFAULT_QUEUE_NAME,
39
  ErrorEventKey,
40
  FailEventKey,
41
  SuccessEventKey,
42
  globalSQEventManager,
43
  queueRequestWaitSetting,
44
  setSilentFactoryStatus,
45
  silentFactoryStatus
46
} from './globalVariables';
47
import {
48
  persistSilentMethod,
49
  push2PersistentSilentQueue,
50
  spliceStorageSilentMethod
51
} from './storage/silentMethodStorage';
52
import stringifyVData from './virtualResponse/stringifyVData';
53
import { regVDataId } from './virtualResponse/variables';
54

55
/** Silent method queue collection */
56
export let silentQueueMap = {} as SilentQueueMap;
2✔
57

58
/**
59
 * Merge queueMap into silentMethod queue collection
60
 * @param queueMap silentMethod queue collection
61
 */
62
export const merge2SilentQueueMap = (queueMap: SilentQueueMap) => {
2✔
63
  forEach(objectKeys(queueMap), targetQueueName => {
18✔
64
    const currentQueue = (silentQueueMap[targetQueueName] = silentQueueMap[targetQueueName] || []);
4✔
65
    pushItem(currentQueue, ...queueMap[targetQueueName]);
4✔
66
  });
18✔
67
};
18✔
68

69
/**
70
 * Clear all items in silentQueue (used for testing)
71
 */
72
export const clearSilentQueueMap = () => {
2✔
73
  silentQueueMap = {};
3✔
74
};
3✔
75

76
/**
77
 * Deeply traverse the target data and replace dummy data with real data
78
 * @param target target data
79
 * @param vDataResponse Collection of dummy data and real data
80
 * @returns Is there any replacement data?
81
 */
82
export const deepReplaceVData = (target: any, vDataResponse: Record<string, any>) => {
2✔
83
  // Search for a single value and replace a dummy data object or dummy data id with an actual value
84
  const replaceVData = (value: any) => {
15✔
85
    const vData = stringifyVData(value);
845✔
86
    // If directly a dummy data object and in a vDataResponse, replace the Map with the value in the vDataResponse
87
    // If it is a string, it may contain virtual data id and in vDataResponse, it also needs to be replaced with the actual value Map
88
    // The virtual data not in this vDataResponse will remain unchanged. It may be the virtual data Map requested next time.
89
    if (vData in vDataResponse) {
845✔
90
      return vDataResponse[vData];
10✔
91
    }
10✔
92
    if (isString(value)) {
845✔
93
      return value.replace(newInstance(RegExpCls, regVDataId.source, 'g'), mat =>
105✔
94
        mat in vDataResponse ? vDataResponse[mat] : mat
14✔
95
      );
105✔
96
    }
105✔
97
    return value;
730✔
98
  };
845✔
99
  if (isObject(target) && !stringifyVData(target, falseValue)) {
15✔
100
    walkObject(target, replaceVData);
15✔
101
  } else {
15!
102
    target = replaceVData(target);
×
103
  }
×
104
  return target;
15✔
105
};
15✔
106

107
/**
108
 * Update the method instance in the queue and replace the dummy data with actual data
109
 * @param vDataResponse A collection of virtual IDs and corresponding real data
110
 * @param targetQueue target queue
111
 */
112
const updateQueueMethodEntities = (vDataResponse: Record<string, any>, targetQueue: SilentQueueMap[string]) =>
2✔
113
  PromiseCls.all(
27✔
114
    mapItem(targetQueue, async silentMethodItem => {
27✔
115
      // Traverse the entity object deeply. If virtual data or virtual data ID is found, replace it with actual data.
116
      deepReplaceVData(silentMethodItem.entity, vDataResponse);
12✔
117
      // If the method instance is updated, re-persist this silent method instance
118
      silentMethodItem.cache && (await persistSilentMethod(silentMethodItem));
12✔
119
    })
27✔
120
  );
27✔
121

122
/**
123
 * Replace dummy data with response data
124
 * @param response real response data
125
 * @param virtualResponse dummy response data
126
 * @returns The corresponding real data set composed of virtual data id
127
 */
128
const replaceVirtualResponseWithResponse = (virtualResponse: any, response: any) => {
2✔
129
  let vDataResponse = {} as Record<string, any>;
57✔
130
  const vDataId = stringifyVData(virtualResponse, falseValue);
57✔
131
  vDataId && (vDataResponse[vDataId] = response);
57✔
132

133
  if (isObject(virtualResponse)) {
57✔
134
    for (const i in virtualResponse) {
32✔
135
      vDataResponse = {
30✔
136
        ...vDataResponse,
30✔
137
        ...replaceVirtualResponseWithResponse(virtualResponse[i], response?.[i])
30✔
138
      };
30✔
139
    }
30✔
140
  }
32✔
141
  return vDataResponse;
57✔
142
};
57✔
143

144
/**
145
 * Start the SilentMethod queue
146
 * 1. Silent submission will be put into the queue and requests will be sent in order. Only after the previous request responds will it continue to send subsequent requests.
147
 * 2. The number of retries is only triggered when there is no response. If the server responds incorrectly or is disconnected, it will not retry.
148
 * 3. When the number of retries is reached and still fails, when nextRound (next round) is set, delay the time specified by nextRound and then request again, otherwise it will try again after refreshing.
149
 * 4. If there is resolveHandler and rejectHandler, they will be called after the request is completed (whether successful or failed) to notify the corresponding request to continue responding.
150
 *
151
 * @param queue SilentMethodqueue
152
 */
153
const setSilentMethodActive = <AG extends AlovaGenerics>(silentMethodInstance: SilentMethod<AG>, active: boolean) => {
2✔
154
  if (active) {
202✔
155
    silentMethodInstance.active = active;
104✔
156
  } else {
161✔
157
    delete silentMethodInstance.active;
98✔
158
  }
98✔
159
};
202✔
160

161
const defaultBackoffDelay = 1000;
2✔
162
export const bootSilentQueue = (queue: SilentQueueMap[string], queueName: string) => {
2✔
163
  /**
164
   * The callback function is controlled by waiting parameters according to the request. If it is not set or is less than or equal to 0, it will be triggered immediately.
165
   * @param queueName queue name
166
   * @param callback callback function
167
   */
168
  const emitWithRequestDelay = (queueName: string) => {
87✔
169
    const nextSilentMethod = queue[0];
126✔
170
    if (nextSilentMethod) {
126✔
171
      const targetSetting = queueRequestWaitSetting.find(({ queue }) =>
69✔
172
        instanceOf(queue, RegExpCls) ? regexpTest(queue, queueName) : queue === queueName
69✔
173
      );
69✔
174
      // eslint-disable-next-line @typescript-eslint/no-use-before-define
175
      const callback = () => queue[0] && silentMethodRequest(queue[0]);
69✔
176
      const delay = targetSetting?.wait ? sloughConfig(targetSetting.wait, [nextSilentMethod, queueName]) : 0;
69✔
177
      delay && delay > 0 ? setTimeoutFn(callback, delay) : callback();
69✔
178
    }
69✔
179
  };
126✔
180

181
  /**
182
   * Run a single silentMethod instance
183
   * @param silentMethodInstance silentMethod instance
184
   * @param retryTimes Number of retries
185
   */
186
  const silentMethodRequest = <AG extends AlovaGenerics>(silentMethodInstance: SilentMethod<AG>, retryTimes = 0) => {
87✔
187
    // Set the current silent method instance to active status
188
    setSilentMethodActive(silentMethodInstance, trueValue);
104✔
189
    const {
104✔
190
      cache,
104✔
191
      id,
104✔
192
      behavior,
104✔
193
      entity,
104✔
194
      retryError = /.*/,
104✔
195
      maxRetryTimes = 0,
104✔
196
      backoff = { delay: defaultBackoffDelay },
104✔
197
      resolveHandler = noop,
104✔
198
      rejectHandler = noop,
104✔
199
      emitter: methodEmitter,
104✔
200
      handlerArgs = [],
104✔
201
      virtualResponse,
104✔
202
      force
104✔
203
    } = silentMethodInstance;
104✔
204

205
    // Trigger pre-request event
206
    globalSQEventManager.emit(
104✔
207
      BeforeEventKey,
104✔
208
      newInstance(GlobalSQEvent<AG>, behavior, entity, silentMethodInstance, queueName, retryTimes)
104✔
209
    );
104✔
210
    promiseThen(
104✔
211
      entity.send(force),
104✔
212
      async data => {
104✔
213
        // The request is successful, remove the successful silent method, and continue with the next request
214
        shift(queue);
39✔
215
        // If the request is successful, remove the successful silent method instance from storage and continue with the next request.
216
        cache && (await spliceStorageSilentMethod(queueName, id));
39✔
217
        // If there is a resolve handler, call it to notify the outside
218
        resolveHandler(data);
8✔
219

220
        // Only when there is a virtualResponse, virtual data is traversed and replaced, and global events are triggered.
221
        // Generally, it is silent behavior, but queue behavior is not required.
222
        if (behavior === BEHAVIOR_SILENT) {
27✔
223
          // Replace dummy data in subsequent method instances in the queue with real data
224
          // Only after unlocking can you access the hierarchical structure of virtualResponse normally.
225
          const vDataResponse = replaceVirtualResponseWithResponse(virtualResponse, data);
27✔
226
          const { targetRefMethod, updateStates } = silentMethodInstance; // It is accurate to obtain it in real time
27✔
227
          // If this silentMethod has targetRefMethod, call updateState again to update the data
228
          // This is an implementation of delayed data updates
229
          if (instanceOf(targetRefMethod, Method) && updateStates && len(updateStates) > 0) {
27✔
230
            const updateStateCollection: UpdateStateCollection<any> = {};
1✔
231
            forEach(updateStates, stateName => {
1✔
232
              // After the request is successful, replace the data with dummy data with real data
233
              updateStateCollection[stateName] = dataRaw => deepReplaceVData(dataRaw, vDataResponse);
1✔
234
            });
1✔
235
            const updated = updateState(targetRefMethod, updateStateCollection);
1✔
236

237
            // If the status modification is unsuccessful, modify the cached data.
238
            if (!updated) {
1!
239
              await setCache(targetRefMethod, (dataRaw: any) => deepReplaceVData(dataRaw, vDataResponse));
×
240
            }
×
241
          }
1✔
242

243
          // Perform dummy data replacement on subsequent silent method instances of the current queue
244
          await updateQueueMethodEntities(vDataResponse, queue);
27✔
245

246
          // Trigger global success event
247
          globalSQEventManager.emit(
27✔
248
            SuccessEventKey,
27✔
249
            newInstance(
27✔
250
              GlobalSQSuccessEvent<AG>,
27✔
251
              behavior,
27✔
252
              entity,
27✔
253
              silentMethodInstance,
27✔
254
              queueName,
27✔
255
              retryTimes,
27✔
256
              data,
27✔
257
              vDataResponse
27✔
258
            )
27✔
259
          );
27✔
260
        }
27✔
261

262
        // Set to inactive state
263
        setSilentMethodActive(silentMethodInstance, falseValue);
39✔
264

265
        // Continue to the next silent method processing
266
        emitWithRequestDelay(queueName);
39✔
267
      },
39✔
268
      reason => {
104✔
269
        if (behavior !== BEHAVIOR_SILENT) {
59✔
270
          // When the behavior is not silent and the request fails, rejectHandler is triggered.
271
          // and removed from the queue and will not be retried.
272
          shift(queue);
2✔
273
          rejectHandler(reason);
2✔
274
        } else {
59✔
275
          // Each request error will trigger an error callback
276
          const runGlobalErrorEvent = (retryDelay?: number) =>
57✔
277
            globalSQEventManager.emit(
57✔
278
              ErrorEventKey,
57✔
279
              newInstance(
57✔
280
                GlobalSQErrorEvent<AG>,
57✔
281
                behavior,
57✔
282
                entity,
57✔
283
                silentMethodInstance,
57✔
284
                queueName,
57✔
285
                retryTimes,
57✔
286
                reason,
57✔
287
                retryDelay
57✔
288
              )
57✔
289
            );
57✔
290

291
          // In silent behavior mode, determine whether retry is needed
292
          // Retry is only effective when the response error matches the retryError regular match
293
          const { name: errorName = '', message: errorMsg = '' } = reason || {};
57!
294
          let regRetryErrorName: RegExp | undefined;
57✔
295
          let regRetryErrorMsg: RegExp | undefined;
57✔
296
          if (instanceOf(retryError, RegExp)) {
57✔
297
            regRetryErrorMsg = retryError;
54✔
298
          } else if (isObject(retryError)) {
57✔
299
            regRetryErrorName = (retryError as RetryErrorDetailed).name;
3✔
300
            regRetryErrorMsg = (retryError as RetryErrorDetailed).message;
3✔
301
          }
3✔
302

303
          const matchRetryError =
57✔
304
            (regRetryErrorName && regexpTest(regRetryErrorName, errorName)) ||
57✔
305
            (regRetryErrorMsg && regexpTest(regRetryErrorMsg, errorMsg));
54✔
306
          // If there are still retry times, try again
307
          if (retryTimes < maxRetryTimes && matchRetryError) {
57✔
308
            // The next retry times need to be used to calculate the delay time, so +1 is needed here.
309
            const retryDelay = delayWithBackoff(backoff, retryTimes + 1);
35✔
310
            runGlobalErrorEvent(retryDelay);
35✔
311
            setTimeoutFn(
35✔
312
              () => {
35✔
313
                retryTimes += 1;
35✔
314
                silentMethodRequest(silentMethodInstance, retryTimes);
35✔
315

316
                methodEmitter.emit(
35✔
317
                  'retry',
35✔
318
                  newInstance(
35✔
319
                    ScopedSQRetryEvent<AG>,
35✔
320
                    behavior,
35✔
321
                    entity,
35✔
322
                    silentMethodInstance,
35✔
323
                    handlerArgs,
35✔
324
                    retryTimes,
35✔
325
                    retryDelay
35✔
326
                  )
35✔
327
                );
35✔
328
              },
35✔
329
              // When there are still retry times, use timeout as the next request time.
330
              retryDelay
35✔
331
            );
35✔
332
          } else {
57✔
333
            setSilentFactoryStatus(2);
22✔
334
            runGlobalErrorEvent();
22✔
335
            // When the number of failures is reached, or the error message does not match the retry, the failure callback is triggered.
336
            methodEmitter.emit(
22✔
337
              'fallback',
22✔
338
              newInstance(ScopedSQErrorEvent<AG, any[]>, behavior, entity, silentMethodInstance, handlerArgs, reason)
22✔
339
            );
22✔
340
            globalSQEventManager.emit(
22✔
341
              FailEventKey,
22✔
342
              newInstance(GlobalSQFailEvent<AG>, behavior, entity, silentMethodInstance, queueName, retryTimes, reason)
22✔
343
            );
22✔
344
          }
22✔
345
        }
57✔
346
        // Set to inactive state
347
        setSilentMethodActive(silentMethodInstance, falseValue);
59✔
348
      }
59✔
349
    );
104✔
350
  };
104✔
351
  emitWithRequestDelay(queueName);
87✔
352
};
87✔
353

354
/**
355
 * Put a new silentMethod instance into the queue
356
 * @param silentMethodInstance silentMethod instance
357
 * @param cache Does silentMethod have cache?
358
 * @param targetQueueName target queue name
359
 * @param onBeforePush Events before silentMethod instance push
360
 */
361
export const pushNewSilentMethod2Queue = async <AG extends AlovaGenerics>(
2✔
362
  silentMethodInstance: SilentMethod<AG>,
64✔
363
  cache: boolean,
64✔
364
  targetQueueName = DEFAULT_QUEUE_NAME,
64✔
365
  onBeforePush: () => any[] | Promise<any>[] = () => []
64✔
366
) => {
64✔
367
  silentMethodInstance.cache = cache;
64✔
368
  const currentQueue = (silentQueueMap[targetQueueName] =
64✔
369
    silentQueueMap[targetQueueName] || []) as unknown as SilentMethod<AG>[];
64✔
370
  const isNewQueue = len(currentQueue) <= 0;
64✔
371
  const beforePushReturns = await Promise.all(onBeforePush());
64✔
372
  const isPush2Queue = !beforePushReturns.some(returns => returns === falseValue);
64✔
373

374
  // Under silent behavior, if there is no fallback event callback bound, it will be persisted.
375
  // If false is returned in onBeforePushQueue, it will no longer be placed in the queue.
376
  if (isPush2Queue) {
64✔
377
    cache && (await push2PersistentSilentQueue(silentMethodInstance, targetQueueName));
62✔
378
    pushItem(currentQueue, silentMethodInstance);
14✔
379
    // If it is a new queue and the status is started, execute it
380
    isNewQueue && silentFactoryStatus === 1 && bootSilentQueue(currentQueue, targetQueueName);
62✔
381
  }
62✔
382
  return isPush2Queue;
64✔
383
};
64✔
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

© 2025 Coveralls, Inc