📜 ⬆️ ⬇️

Canceled Promises in EcmaScript6

Vladislav Vlasov, a software engineer at Developer Soft and a teacher at the Netology course, wrote a series of articles on EcmaScript6 especially for the blog. In the first part, the examples examined the dynamic analysis of code in EcmaScript using Iroh.js. This article will explain how to implement canceled Promises .

Asynchronous and event scheduler in EcmaScript


The concept of Promise (promises) is one of the key in modern EcmaScript. Promise allows you to ensure the consistent implementation of asynchronous actions by organizing them into chains, which in addition provide an interception of errors. The modern syntax of async / await operators is also technically based on Promise, and is only syntactic sugar.


')
With all its rich functionality, Promise have one drawback - they do not support the ability to cancel the running action. In order to understand how to get around this restriction, it is necessary to consider how asynchronous actions arise and function in EcmaScript, because Promise is just a wrapper for them.

The EcmaScript language engine, be it V8 or Chakra, is single-threaded, and allows you to perform only one action at a time. In a browser environment, quite modern engines support the WebWorkers technology, and in Node.js you can create a separate child process, and this will allow you to parallelize the execution of the code. However, the created execution thread is an independent process that can exchange information with the thread that created it only through messages, so this is not in itself a multi-threaded model.

Instead, traditional EcmaScript is based on a multiplexing model: to perform several actions in parallel, they are broken up into small fragments, each of which is performed relatively quickly and never blocks the flow of execution. By mixing such fragments, the actions associated with them are actually performed in parallel.

Since custom code and host environment functions, such as rendering a visual interface (UI) of a web page, are performed in the same stream, for example, a long or infinite loop in custom code leads to suspension of web rendering operations. page and its hang. To separate the time periods in which certain code fragments will be executed, the event loop is used. So how can an executable fragment appear in the event loop?

Normal client code only performs a sequential set of actions, consisting of a thread of execution with conditions, loops, and function calls. In order to implement deferred execution, you must register the client callback function in the host environment.

In a browser-based environment, this usually boils down to one of three possibilities: timers, events, and asynchronous requests for resources. Timers provide a function call after a time ( setTimeout ), in the first free slot in the event scheduler ( setImmediate ), or even in the process of drawing a web page ( requestAnimationFrame ). Events are a reaction to the action that occurred, as a rule, in the DOM model, and can be triggered both by the user (event: click on the button) and by the internal processes of displaying UI elements (event: recalculation of styles is completed). Requests to resources are made into a separate category, but in reality they refer to events, with the only difference being that the initial initiator is the client code itself.

This is clearly shown in the diagram below:



Asynchronous action wrapper


Further, it is important to consider how the above asynchronous actions turn into a Promise. In order to touch on the maximum number of aspects to cancel a Promise, the following code will combine the use of timers, DOM model events, and arbitrary client code that links them. The example assumes the execution of an AJAX request that returns a large amount of data in CSV format, and subsequent processing in a potentially slow function in a line-by-line form to prevent the main stream from hanging.

 function fetchInformation() { function parseRow(rawText) {   /* Some function for row parsing which works very slow  */ } const xhrPromise = new Promise((resolve, reject) => {   const xhr = new XMLHttpRequest();   xhr.open('GET', '.../some.csv'); // API endpoint URL with some big CSV database   xhr.onload = () => {     if (xhr.status >= 200 && xhr.status < 300) {       resolve(String(xhr.response));     } else {       reject(new Error(xhr.status));     }   };   xhr.onerror = () => {     reject(new Error(xhr.status));   };   xhr.send(); }); const delayImpl = window.setImmediate ? setImmediate : requestAnimationFrame; const delay = () => new Promise(resolve => delayImpl(resolve)) const parsePromise = (response) => new Promise((resolve, reject) => {   let flowPromise = Promise.resolve();   let lastDemileterIdx = 0;   let result = [];   while(lastDemileterIdx >= 0) { const newIdx = response.indexOf('\n', lastDemileterIdx); const row = response.substring(   lastDemileterIdx,   (newIdx > -1 ? newIdx - lastDemileterIdx : Infinity) ); flowPromise = flowPromise.then(() => {   result.push(parseRow(row));   return delay(); }); lastDemileterIdx = newIdx;   }   flowPromise.then(resolve, reject); }); return xhrPromise.then(parsePromise); } 

As a DOM model event, the successful or erroneous completion of an AJAX request is used, and the timers provide sequential batch processing of large amounts of data to provide working time for the UI thread. It is easy to see that, from the external point of view, such a Promise is a monolithic element, upon completion of which the processed database is available in the proper format, or a description of the error, if a failure occurred during execution.

From the point of view of the caller, it is convenient to be able to cancel such a Promise as a whole. For example, in case the user has closed the visual element that required this data to be displayed. However, from the point of view of the internal structure, Promise is a set of synchronous and asynchronous actions, some of which may have already been started and completed. Since this sequence defines an arbitrary client code, emergency completion steps must also be described manually.

Implementing a cancelable Promise


It is important to remember that the interruption of synchronous code, such as loops, cannot occur in principle, because if the code is already being executed (and the EcmaScript engine is single-threaded), then at this moment no other code can be executed that would execute its interruption. Thus, terminations require only the truly asynchronous actions described above: timers, events, and access to external resources.

Timer setup functions have dual operations to cancel them: clearTimeout , clearImmediate and cancelAnimationFrame respectively. For DOM model events, simply delete the subscription to the appropriate callback function. Also, for timers, you can use a simpler approach - pre-wrap them in a Promise object that has a manual isCancelled flag. If after the expiry of the timer the Promise should be canceled, then the callback function is simply not executed. In this case, the timer remains in the scheduler, but in case of cancellation after it ends, nothing happens.

In the case of accessing external resources, the situation is more complicated: in any case, you can ignore the result of the operation by completing a reply from the corresponding event, but it is not always possible to interrupt the operation itself. From the point of view of the logic of Promise execution, this may be insignificant, but an uninterrupted operation consumes excessive resources.

In particular, the fetch method, which is designed to replace the classic XMLHttpRequest for conducting AJAX requests, and providing immediate return of the Promise object without the need for an additional wrapper, does not allow cancellation of the request. For this reason, you must use the abort method in the XMLHttpRequest object to actually cancel the HTTP request.

The final code with support for Promise cancellation may look like this. For better clarity, only the changed code is shown, and the old one is replaced by a comment with an ellipsis.

 function fetchInformation() { /* ... */ let isCancelled = false; let xhrAbort; const xhrPromise = new Promise((resolve, reject) => { /* ... */ xhrAbort = xhr.abort.bind(xhr); }); const delayImpl = window.setImmediate ? setImmediate : requestAnimationFrame; const delay = () => new Promise((resolve, reject) => delayImpl(() => (!isCancelled ? resolve(): reject(new Error('Cancelled')))) ); /* ... */ const promise = xhrPromise.then(parsePromise); promise.cancel = () => { try { xhrAbort(); } catch(err) {}; isCancelled = true; } return promise; } 

Since Promise is a normal object from the EcmaScript point of view, the cancel method can easily be added to it. Also, since only one resulting Promise object is returned to the external environment, the cancel method is added only for it, and all the internal logic is encapsulated in the current lexical block of the generating function.

Results


Implementing a cancelable Promise in EcmaScript is a relatively simple task that can be easily accomplished even for an asynchronous chain that has non-trivial sequential call logic inside: by saving the cancel flag in objects and activation contexts of generating functions. Cancellation can be both superficial, when Promise is interrupted with an error and does not perform third-party effects, or deep, when all initiated asynchronous operations (timers, accessing external resources, etc.) are really canceled.

A key aspect of canceled Promise is the need for a full manual cancellation operation. It cannot be achieved automatically, for example, by implementing the own class Promise. Theoretically, the problem can be solved by executing code in a virtual machine, in which all asynchronous actions initiated in the Promise initialization stack and dependent then-branches will be recorded, but this is a rather nontrivial implementation and of little use in practice.

Thus, canceled Promises in EcmaScript is just an interface agreement that allows you to interrupt and cancel Promise effects, which are encapsulated chains of logical actions. In general, the concept of cancellability does not exist.

From the Editor


Courses "Netology" on the topic:

Source: https://habr.com/ru/post/348632/


All Articles