Last article in this series: [code appreciation] simple and elegant JavaScript code fragment (I): asynchronous control
Flow control (also known as current limiting, controlling call frequency)
In order to ensure the stable operation of the system, the back-end often limits the call frequency (for example, no more than 10 times per second per person). In order to avoid wasting resources or being punished by the system, the front end also needs to actively limit the frequency of calling API.
When the front end needs to pull lists in large quantities, or when it needs to call API to query details for each list item, it is particularly necessary to limit the current.
A flow control tool function wrapFlowControl is provided here. Its advantages are:
- Simple to use and transparent to the caller: you only need to wrap your original asynchronous function to get the flow control restricted function, which is used in the same way as the original asynchronous function. Applies to any asynchronous function. const apiWithFlowControl = wrapFlowControl(callAPI, 2);
- No call is ignored (unlike Anti shake or throttle ). Each call will be executed and the corresponding results will be obtained. However, it may be delayed in order to control the frequency.
Use example:
// A scheduling queue was created const apiWithFlowControl = wrapFlowControl(callAPI, 2); // ...... <button onClick={() => { const count = ++countRef.current; // Scheduling a function call with a scheduling queue apiWithFlowControl(count).then((result) => { // do something with api result }); }} > Call apiWithFlowControl </button>
Code implementation of wrapFlowControl:
const ONE_SECOND_MS = 1000; /** * Controls the frequency of function calls. In any one second interval, fn will not be called more than maxexecpercec times. * If the function trigger frequency exceeds the limit, some calls will be delayed so that the actual call frequency meets the above requirements. */ export function wrapFlowControl<Args extends any[], Ret>( fn: (...args: Args) => Promise<Ret>, maxExecPerSec: number ) { if (maxExecPerSec < 1) throw new Error(`invalid maxExecPerSec`); const queue: QueueItem[] = []; const executed: ExecutedItem[] = []; return function wrapped(...args: Args): Promise<Ret> { return enqueue(args); }; function enqueue(args: Args): Promise<Ret> { return new Promise((resolve, reject) => { queue.push({ args, resolve, reject }); scheduleCheckQueue(); }); } function scheduleCheckQueue() { const nextTask = queue[0]; // The scheduleCheckQueue recursive call is stopped only when the queue is empty if (!nextTask) return; cleanExecuted(); if (executed.length < maxExecPerSec) { // The next task can only be executed if the number of executions in the last second is less than the threshold queue.shift(); execute(nextTask); scheduleCheckQueue(); } else { // We'll schedule it later const earliestExecuted = executed[0]; const now = new Date().valueOf(); const waitTime = earliestExecuted.timestamp + ONE_SECOND_MS - now; setTimeout(() => { // At this time, early executed can be cleared to provide quota for the execution of the next task scheduleCheckQueue(); }, waitTime); } } function cleanExecuted() { const now = new Date().valueOf(); const oneSecondAgo = now - ONE_SECOND_MS; while (executed[0]?.timestamp <= oneSecondAgo) { executed.shift(); } } function execute({ args, resolve, reject }: QueueItem) { const timestamp = new Date().valueOf(); fn(...args).then(resolve, reject); executed.push({ timestamp }); } type QueueItem = { args: Args; resolve: (ret: Ret) => void; reject: (error: any) => void; }; type ExecutedItem = { timestamp: number; }; }
Delay determination function logic
As can be seen from the above example, when using wrapFlowControl, you need to define the logic of asynchronous function callAPI in advance to get the flow control function.
However, in some special scenarios, we need to determine what logic the asynchronous function should execute when initiating the call. Postpone "OK at definition" to "OK at call". Therefore, we implemented another tool function, createFlowControlScheduler.
In the above usage example, DemoWrapFlowControl is an example: when the user clicks the button, we decide whether to call API1 or API2.
// Create a scheduling queue const scheduleCallWithFlowControl = createFlowControlScheduler(2); // ...... <div style={{ marginTop: 24 }}> <button onClick={() => { const count = ++countRef.current; // The asynchronous operation to be performed is determined at the time of invocation // Add asynchronous operations to the scheduling queue scheduleCallWithFlowControl(async () => { // Flow control will guarantee the execution frequency of this asynchronous function if (count % 2 === 1) { return callAPI1(count); } else { return callAPI2(count); } }).then((result) => { // do something with api result }); }} > Call scheduleCallWithFlowControl </button> </div>
Each time scheduleCallWithFlowControl receives an asynchronous function, it will be added to the scheduling queue. It ensures that all asynchronous functions in the scheduling queue are called (in the order they are added to the queue) and that the call frequency does not exceed the specified value.
The implementation of createFlowControlScheduler is actually very simple, based on the previous wrapFlowControl implementation:
/** * It is similar to wrapFlowControl, except that the definition of task is delayed until the wrapper is called, * It is not provided when the flowControl wrapper is created */ export function createFlowControlScheduler(maxExecPerSec: number) { return wrapFlowControl(async <T>(task: () => Promise<T>) => { return task(); }, maxExecPerSec); }