Hi Habr!
Today we would like to speculate about
promises and code reuse.
It so happened that these two things together helped us greatly several times, and we would like to share how it was.

')
The moral is simple: if you are not using promises yet, start doing it!
Upd: as pragmadash was rightly noted in the comments, the article is about jquery.Deferred , which strictly speaking, are not promises . In the context of the article, this distinction can be neglected, but in order not to mislead those who are not familiar with promises, this remark is necessary.
We have already briefly discussed this topic in our two articles (
one ,
zero ). Actually, this article was supposed to be the final section to the
previous one . But there is heavy and big reading, so we decided to split it into two.
Here we will tell some real stories about how they saved us.
A small preface: in one of our applications, all requests to the back end are sent through a central point — the
callService method of the base module, from which all services inherit. This method accepts request parameters for input, normalizes them, and returns a promise to which the caller can subscribe. Like this:
callService: function (settings) { // var callSettings = { type: settings.method || ETR.HttpVerb.POST, contentType: 'application/json; charset=utf-8', dataType: settings.dataType || undefined, url: settings.relativeUrl ? ETR.serviceRootURL + settings.relativeUrl : settings.url, data: settings.data, context: settings.context || this.requestContext, beforeSend: settings.beforeSend || undefined }; // return jquery.ajax(callSettings); }
And that's how such an organization helped us out.
Case 1. Access to disposed objects
Problem:
When we wrote the application described in our
previous article , we implemented it as a SPA. Accordingly, we had to work very hard on cleaning up the memory, and literally everything had
dispose conveyor. In this case, it is possible that a request is sent from the page to the server, and the user suddenly leaves the page (for example, he poked the wrong menu item). View model of the current window passes the
dispose pipeline , the user goes to another page.
And here from the server the result of the query is returned.
To talk about what can happen in such a situation is meaningless - anything can happen. The best thing that happened was that the error was flying, and it became clear that something was wrong.
Decision:
In the above-mentioned method,
callService added a few lines of code. Briefly, their essence is that we remember every promise. Upon completion, or when calling the service dispose method, they were again forgotten.
navThrottleKey: 'Any abracadabra. Guid for example or some string like this.', callService: function (settings) { ... var promise = jquery.ajax(callSettings); var requestId = ++requestCounter; this.rejectsOnDispose[requestId] = promise; promise.always(_.bind(function () { delete this.rejectsOnDispose[requestId]; }, this)); return promise; } dispose: function () { for (var indexer in this.rejectsOnDispose) { this.rejectsOnDispose[indexer].abort(this.serviceThrottleKey); delete this.rejectsOnDispose[indexer]; } this.rejectsOnDispose = null; this.requestContext = null; }
Voila A dozen lines of code solved the problem throughout the application.
Note an additional comment field
this.serviceThrottleKey
This is a guaranteed unique string that allows you to “recognize” the situation being described and not call the
fail handlers that invariably start when you call the
abort promise method. That is, we intercept our own fault and jam it in order not to display an error message to the user on the UI.
Case 2. Usability
Problem:
There is such a situation that a call to the service takes a long time. And until it is executed, we cannot let the user do anything (for example, when it is loading initial data). At the same time, I want to show the user a message about the need to wait and lock the screen.
And sometimes it happens that the call is sometimes performed for a long time, and sometimes quickly (from the cache). For such cases, it is advisable not to show the user the message that appears and disappears immediately, as this is tiring.
Decision:
The solution is still in our callService method. We will add five more lines of code (we’ll remove the previous ones for ease of reading the code).
callService: function (settings, statusMessage) { ... var sid = statusMessage ? statusTracker.trackStatus(statusMessage) : null; var promise = jquery.ajax(callSettings); promise.done(_.bind(function () { if (sid !== null) { statusTracker.resolveStatus(sid, ETR.ProgressState.Done); } }, this)).fail(_.bind(function () { if (sid !== null) { statusTracker.resolveStatus(sid, ETR.ProgressState.Fail); } }, this)); return promise; }
The point is simple: if a certain statusMessage string is passed to us as a parameter, this call wants to show a message when it is called.
Show as follows:
statusTracker.trackStatus(statusMessage)
Hiding like this:
statusTracker.resolveStatus(sid, ETR.ProgressState.Fail);
The
statusTracker object
, in turn, works according to the following logic:
- When calling the trackStatus method , we do not show the message, but assign it a unique number and return it (use the number that is returned by the setTimeout call).
- After a certain timing (we have 300 ms), if the resolveStatus command did not arrive, then we begin to display this message and block the screen.
- When the resolveStatus command arrives, we change the status icon (success or fail) and hide the message in animation mode with a slight delay. This saves us from the “blinking” screen, when the delay time (300ms) almost coincided with the query execution time.
It looks like this:

Case 3. Double calls to services
Problem:
We have a situation where several objects on the page can ask for the same data. I don’t want to go to the server for the same data (as well as showing the same status as described above). Let's try to think of something.
Decision:
The solution is still in the same
callService method. The bottom line is simple. Determining that a query can be called several times, and in doing so it will be satisfactory to return the same result - the cause of the caller.
We supplement the callService method
with another ten lines:
callService: function (settings, statusMessage, singletKey) { … if (singletKey) { var singlet = wcfDispatcherDef.singletsCalls[singletKey]; if (singlet) { var singletResolver = jquery.Deferred(); singlet.done(function () { singletResolver.resolveWith(callSettings.context, arguments); }).fail(function () { singletResolver.rejectWith(callSettings.context, arguments); }); return singletResolver; } } if (singletKey) { wcfDispatcherDef.singletsCalls[singletKey] = promise; } … }
Note: this approach is best used with caution, since in the case of reference types you can catch a lot of unobvious problems. To solve potential problems, if performance allows, you can use jquery.extend with the deep flag or the banal JSON.parse (JSON.stringify ()) .Case 4. About data caching
Problem:
Perhaps a great description is not required here. It is necessary to cache data on the client for a certain time.
Decision:
The solution was the method that lies in our base class of services. At the input, it takes the function of the same contract as the
callService , and the caching parameters. It returns a wrapper function that will look into the cache before calling the service:
wrapWithCache: function (fn, cacheKey, expirationPolicy) { return _.wrap(fn, function (initialFn) { var cacheResult = cacheService.getCacheValue(cacheKey); if (cacheResult !== undefined) { var q = jquery.Deferred(); _.delay(function (scope) { q.resolveWith(scope, [cacheResult]); }, 0, this.requestContext || this); return q; } else { return initialFn.apply(this, Array.prototype.slice.call(arguments, 1)) .done(_.bind(function (result) { cacheService.setCacheValue(cacheKey, result, expirationPolicy); }, this)); } }); }
Note 1 : in fact, using underscore delay (or native setTimeout) for the case of using jquery.ajax is not absolutely mandatory, since all the handlers for jquery.Deffered declared after its processing will still be called . But still, the inherently asynchronous model of work is not worth breaking, so we use _.delay.As a result, if we want to wrap up a call with a cache, this one, for example:
{ return this.callService({ method: ETR.HttpVerb.POST, url: this.someServiceUrl + '/getSomeList', data: request }).fail(this.commonFaultHandler); }
then we add one line of code to the service:
this.getSomeList = this.wrapWithCache(this.getSomeList, 'some-cache-key', moment().add(30, 'm'));
Note 2: The problems specified in the previous history are also valid for this one: be careful with reference types.Case 5. Error Handling
Problem:
Somehow a story happened with us: there lived an application, lived well, did not complain.
And then there was a wish - some users should be able to watch the data of another user. At the same time, the user himself has nothing to show us here, but if he chooses whose show, we will definitely show it. On the server, it was all decided simply. A normal user who does not have the right to watch something gets a
403 Forbidden error, and the result is something like this:

And for cool users, the result should be like this:

After selection, the user is assigned a certain token and ... Yes, it does not matter. A story about how to show a selection dialog.
Throughout the application ... Already written and released in production ...
Decision:
Saved by the fact that on the client we already had a common error handling pipeline. Its essence came down to an array of handlers, each of which looked to see if it could handle the error that occurred. If one particular smog, the method returns true, and the others are not called. If it failed, we return false, and the attempt to handle the error continues.
Note: in the case when it is necessary to handle a completely specific error in a completely specific place, it is up to the fail handler of a particular service to call the common-new processing pipeline. He does this if he does not recognize in error what he is meant to process.All we needed to solve the problem was to write a method that checked user rights at the start of the application. And, in which case, I inserted another
403 status handler into the pipeline, which will show the coveted dialozhk.
A couple of hours of work and - done.
Thanks to everyone who read it. See you soon!