• 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.45
/packages/client/src/hooks/silent/createSilentQueueMiddlewares.ts
1
import {
2✔
2
  AlovaEventBase,
3
  ScopedSQCompleteEvent,
4
  ScopedSQErrorEvent,
5
  ScopedSQEvent,
6
  ScopedSQSuccessEvent
7
} from '@/event';
8
import {
9
  PromiseCls,
10
  createEventManager,
11
  decorateEvent,
12
  falseValue,
13
  getConfig,
14
  isFn,
15
  len,
16
  newInstance,
17
  objectKeys,
18
  promiseResolve,
19
  promiseThen,
20
  regexpTest,
21
  sloughConfig,
22
  trueValue,
23
  undefinedValue,
24
  walkObject
25
} from '@alova/shared';
26
import { AlovaGenerics, Method } from 'alova';
27
import {
28
  AlovaFrontMiddleware,
29
  AlovaMethodHandler,
30
  BeforePushQueueHandler,
31
  FallbackHandler,
32
  PushedQueueHandler,
33
  RetryHandler,
34
  SQHookBehavior,
35
  SQHookConfig,
36
  ScopedSQEvents,
37
  UseHookExposure
38
} from '~/typings/clienthook';
39
import { MethodHandler, SilentMethod } from './SilentMethod';
40
import {
41
  BEHAVIOR_QUEUE,
42
  BEHAVIOR_SILENT,
43
  BEHAVIOR_STATIC,
44
  DEFAULT_QUEUE_NAME,
45
  setVDataIdCollectBasket,
46
  silentAssert,
47
  vDataIdCollectBasket
48
} from './globalVariables';
49
import { pushNewSilentMethod2Queue } from './silentQueue';
50
import createVirtualResponse from './virtualResponse/createVirtualResponse';
51
import stringifyVData from './virtualResponse/stringifyVData';
52
import { regVDataId } from './virtualResponse/variables';
53

54
/**
55
 * A global silentMethod instance that will have a value from before the first success event is triggered to after the last success event is triggered (synchronization period)
56
 * In this way, the current silentMethod instance can be obtained in updateStateEffect in onSuccess.
57
 */
58
export let currentSilentMethod: SilentMethod<any> | undefined = undefinedValue;
2✔
59

60
/**
61
 * Create SilentQueue middleware function
62
 * @param config Configuration object
63
 * @returns middleware function
64
 */
65
export default <AG extends AlovaGenerics, Args extends any[]>(
2✔
66
  handler: Method<AG> | AlovaMethodHandler<AG, Args>,
24✔
67
  config?: SQHookConfig<AG>
24✔
68
) => {
24✔
69
  const { behavior = 'queue', queue = DEFAULT_QUEUE_NAME, retryError, maxRetryTimes, backoff } = config || {};
24!
70
  const eventEmitter = createEventManager<ScopedSQEvents<AG>>();
24✔
71
  let handlerArgs: any[] | undefined;
24✔
72
  let behaviorFinally: SQHookBehavior;
24✔
73
  let queueFinally = DEFAULT_QUEUE_NAME;
24✔
74
  let forceRequest = falseValue;
24✔
75
  let silentMethodInstance: SilentMethod<AG>;
24✔
76

77
  /**
78
   * method instance creation function
79
   * @param args Call the function passed in by send
80
   * @returns method instance
81
   */
82
  const createMethod = (...args: any[]) => {
24✔
83
    silentAssert(isFn(handler), 'method handler must be a function. eg. useSQRequest(() => method)');
49✔
84
    setVDataIdCollectBasket({});
49✔
85
    handlerArgs = args;
49✔
86
    return (handler as MethodHandler<AG>)(...args);
49✔
87
  };
49✔
88

89
  // Decorate success/error/complete event
90
  const decorateRequestEvent = (requestExposure: UseHookExposure<AG, Args>) => {
24✔
91
    // Set event callback decorator
92
    requestExposure.onSuccess = decorateEvent(requestExposure.onSuccess, (handler, event) => {
24✔
93
      currentSilentMethod = silentMethodInstance;
27✔
94
      handler(
27✔
95
        newInstance(
27✔
96
          ScopedSQSuccessEvent<AG, Args>,
27✔
97
          behaviorFinally,
27✔
98
          event.method,
27✔
99
          silentMethodInstance,
27✔
100
          event.args,
27✔
101
          event.data
27✔
102
        ) as any
27✔
103
      );
27✔
104
    });
24✔
105

106
    requestExposure.onError = decorateEvent(requestExposure.onError, (handler, event) => {
24✔
107
      handler(
1✔
108
        newInstance(
1✔
109
          ScopedSQErrorEvent<AG, Args>,
1✔
110
          behaviorFinally,
1✔
111
          event.method,
1✔
112
          silentMethodInstance,
1✔
113
          event.args,
1✔
114
          event.error
1✔
115
        )
1✔
116
      );
1✔
117
    });
24✔
118
    requestExposure.onComplete = decorateEvent(requestExposure.onComplete, (handler, event) => {
24✔
119
      handler(
4✔
120
        newInstance(
4✔
121
          ScopedSQCompleteEvent<AG, Args>,
4✔
122
          behaviorFinally,
4✔
123
          event.method,
4✔
124
          silentMethodInstance,
4✔
125
          event.args,
4✔
126
          event.status,
4✔
127
          event.data,
4✔
128
          event.error
4✔
129
        ) as any
4✔
130
      );
4✔
131
    });
24✔
132
  };
24✔
133

134
  /**
135
   * middleware function
136
   * @param context Request context, containing request-related values
137
   * @param next continue executing function
138
   * @returns Promise object
139
   */
140
  const middleware: AlovaFrontMiddleware<AG, Args> = ({ method, args, cachedResponse, proxyStates, config }, next) => {
24✔
141
    const { silentDefaultResponse, vDataCaptured, force = falseValue } = config;
27✔
142

143
    // Because the behavior return value may change, it should be called for each request to re-obtain the return value.
144
    const baseEvent = AlovaEventBase.spawn(method, args);
27✔
145
    behaviorFinally = sloughConfig(behavior, [baseEvent]);
27✔
146
    queueFinally = sloughConfig(queue, [baseEvent]);
27✔
147
    forceRequest = sloughConfig(force, [baseEvent]);
27✔
148

149
    // Empty temporary collection variables
150
    // They need to be cleared before returning
151
    const resetCollectBasket = () => {
27✔
152
      setVDataIdCollectBasket((handlerArgs = undefinedValue));
27✔
153
    };
27✔
154

155
    // If v data captured is set, first determine whether the request-related data contains virtual data.
156
    if (isFn(vDataCaptured)) {
27✔
157
      let hasVData = vDataIdCollectBasket && len(objectKeys(vDataIdCollectBasket)) > 0;
3✔
158
      if (!hasVData) {
3✔
159
        const { url, data } = method;
1✔
160
        const { params, headers } = getConfig(method);
1✔
161
        walkObject({ url, params, data, headers }, value => {
1✔
162
          if (!hasVData && (stringifyVData(value, falseValue) || regexpTest(regVDataId, value))) {
5✔
163
            hasVData = trueValue;
1✔
164
          }
1✔
165
          return value;
5✔
166
        });
1✔
167
      }
1✔
168

169
      // If v data captured has return data, use it as the response data, otherwise continue the request
170
      const customResponse = hasVData ? vDataCaptured(method) : undefinedValue;
3!
171
      if (customResponse !== undefinedValue) {
3✔
172
        resetCollectBasket(); // Reset when captured by v data captured
3✔
173
        return promiseResolve(customResponse);
3✔
174
      }
3✔
175
    }
3✔
176

177
    if (behaviorFinally !== BEHAVIOR_STATIC) {
27✔
178
      // Wait for the method in the queue to complete execution
179
      const createSilentMethodPromise = () => {
22✔
180
        const queueResolvePromise = newInstance(PromiseCls, (resolveHandler, rejectHandler) => {
22✔
181
          silentMethodInstance = newInstance(
22✔
182
            SilentMethod<AG>,
22✔
183
            method,
22✔
184
            behaviorFinally,
22✔
185
            eventEmitter,
22✔
186
            undefinedValue,
22✔
187
            !!forceRequest,
22✔
188
            retryError,
22✔
189
            maxRetryTimes,
22✔
190
            backoff,
22✔
191
            resolveHandler,
22✔
192
            rejectHandler,
22✔
193
            handlerArgs,
22✔
194
            vDataIdCollectBasket && objectKeys(vDataIdCollectBasket)
22✔
195
          );
22✔
196
          resetCollectBasket(); // Reset when Behavior is queue and silent
22✔
197
        });
22✔
198

199
        // On before push and on pushed events are bound synchronously, so they need to be queued asynchronously to trigger the event normally.
200
        promiseThen(promiseResolve(undefinedValue), async () => {
22✔
201
          const createPushEvent = () =>
22✔
202
            newInstance(ScopedSQEvent<AG, Args>, behaviorFinally, method, silentMethodInstance, args);
42✔
203

204
          // Put the silent method into the queue and persist it
205
          const isPushed = await pushNewSilentMethod2Queue(
22✔
206
            silentMethodInstance,
22✔
207
            // After the onFallback event is bound, even the silent behavior mode is no longer stored.
208
            // onFallback will be called synchronously, so it needs to be determined asynchronously whether there are fallbackHandlers
209
            len(eventEmitter.eventMap.fallback || []) <= 0 && behaviorFinally === BEHAVIOR_SILENT,
22✔
210
            queueFinally,
22✔
211

212
            // Execute the callback before putting it into the queue. If false is returned, it will prevent putting it into the queue.
213
            () => eventEmitter.emit('beforePushQueue', createPushEvent())
22✔
214
          );
22✔
215
          // Only after putting it into the queue, the callback after putting it into the queue will be executed.
216
          isPushed && eventEmitter.emit('pushedQueue', createPushEvent());
22✔
217
        });
22✔
218

219
        return queueResolvePromise;
22✔
220
      };
22✔
221

222
      if (behaviorFinally === BEHAVIOR_QUEUE) {
22✔
223
        // Forced request, or loading status needs to be updated when cache is hit
224
        const needSendRequest = forceRequest || !cachedResponse;
14✔
225
        if (needSendRequest) {
14✔
226
          // Manually set to true
227
          proxyStates.loading.v = trueValue;
14✔
228
        }
14✔
229

230
        // When using the cache, use the cache directly, otherwise enter the request queue
231
        return needSendRequest ? createSilentMethodPromise() : promiseThen(promiseResolve(cachedResponse));
14!
232
      }
14✔
233

234
      const silentMethodPromise = createSilentMethodPromise();
8✔
235
      // Create virtual response data in silent mode. Virtual response data can generate arbitrary virtual data.
236
      const virtualResponse = (silentMethodInstance.virtualResponse = createVirtualResponse(
8✔
237
        isFn(silentDefaultResponse) ? silentDefaultResponse() : undefinedValue
22✔
238
      ));
22✔
239
      promiseThen<any>(silentMethodPromise, realResponse => {
22✔
240
        // Update after obtaining real data
241
        proxyStates.data.v = realResponse;
7✔
242
      });
22✔
243

244
      // In Silent mode, the virtual response value is returned immediately, and then updated when the real data is returned.
245
      return promiseResolve(virtualResponse);
22✔
246
    }
22✔
247
    resetCollectBasket(); // Reset when Behavior is static
2✔
248
    return next();
2✔
249
  };
27✔
250

251
  return {
24✔
252
    c: createMethod,
24✔
253
    m: middleware,
24✔
254
    d: decorateRequestEvent,
24✔
255

256
    // event binding function
257
    b: {
24✔
258
      /**
259
       * Bind fallback event
260
       * @param handler Fallback event callback
261
       */
262
      onFallback: (handler: FallbackHandler<AG>) => {
24✔
263
        eventEmitter.on('fallback', handler);
2✔
264
      },
2✔
265

266
      /**
267
       * Event before binding to queue
268
       * @param handler Event callback before enqueuing
269
       */
270
      onBeforePushQueue: (handler: BeforePushQueueHandler<AG>) => {
24✔
271
        eventEmitter.on('beforePushQueue', handler);
7✔
272
      },
7✔
273

274
      /**
275
       * Event after binding to queue
276
       * @param handler Event callback after being queued
277
       */
278
      onPushedQueue: (handler: PushedQueueHandler<AG>) => {
24✔
279
        eventEmitter.on('pushedQueue', handler);
3✔
280
      },
3✔
281

282
      /**
283
       * retry event
284
       * @param handler Retry event callback
285
       */
286
      onRetry: (handler: RetryHandler<AG>) => {
24✔
287
        eventEmitter.on('retry', handler);
×
288
      }
×
289
    }
24✔
290
  };
24✔
291
};
24✔
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