Published on behalf of
jbubsk . We have been at Tinkoff Bank for a hundred years struggling with the problem of the context of the current request to the server. This is the moment - when the ticket comes about that, “what is this mistake and why is it here?” Is very subtle, and it beats the developers for the patient. This article will tell you how we managed to solve this problem.
To understand a person, you need to think like a person.
Imagine that being on a certain page of the application, the user clicked on the button for submitting a form or searching the list. The response time may be long and it does not matter for what reason: whether it is an expensive and resource-intensive operation, or the Internet is slow, or maybe the user is a superhero, and the time is too slow for him.
And now the request is already on the way, the data is being processed with might and main, but processed with an error that needs to be shown to the user. The Internet bank client, without waiting for an answer, goes to another page in the application. He happily observes the account statement, and suddenly a mistake pops up: "Dear Pyotr Mikhalych, the operation was rejected, and the reason for this is the moon in Capricorn and the financial crisis." But Pyotr Mikhalych is already fascinated with an extract and does not want to know that the request from the previous page did not work.
This is bad. Application context is different. It is advisable to cancel the request that remains in pending in order to avoid an error message on a page to which the error no longer applies.
')
How we do online banking for business
With the arrival of Angular 1.5, we started using its wonderful
components . They perfectly fell into the hands of developers, the code became laconic, and our team with great pleasure absorbs all the innovations. Unfortunately, in any development, the introduction of something new is not without difficulties.
The situation with Peter Mikhalych is important regardless of whether the angular has components or not. You can, of course, pass some kind of reference to the component instance, and from the same component, from function to function, down the chain to the service itself, which makes a request to the server. If necessary, for this link, simply cancel through this service a request from the pool in which it will be stored. The component itself provides a convenient
$ onDestroy hook for such purposes.
Although this is quite a working script, I want the code to have less such vermicelli. The call chain can be long.
Creating a request context for a server is not easy by itself. A new solution should be built into an already large working system. The task was greatly complicated by the fact that it was impossible to break the working code. Fortunately, I remembered the article about the context in the second angular. After digging in this direction,
Zone.js came to the
rescue - the same one that was successfully screwed into
Angular 2 Many do not understand what kind of animal it is. Even despite the absence of examples different from the measurement of timing, it turned out to apply the context of the zones. Some misunderstandings about the nature of the zones still remained and live in my head.
Here is our solution to the problem:
class AccountsController extends BaseController { ... constructor(private accountsService: AccountsService) { super() } @cancelable getAccounts() { this.accountsService.list(...).then(accounts => this.accounts = accounts); } } class BaseController implements ng.IComponentController { public serializeId: string; constructor() { this.serializeId = `${Date.now()}_${Math.random()}`; } $onDestroy() { const injector: ng.auto.IInjectorService = angular.element(window.document.body).injector(); const apiService: ApiService = <ApiService>injector.get('ApiService'); apiService.cancelRequests(this.serializeId); } }
The
@cancelable decorator helps us, at the highest level, to put a call to a component method in a zone, tying a zone to a component instance. Binding looks much better than the chain of forwarding this reference from function to function.
It looks like a decorator:
export function cancelable(targetInstance: any, key: string, targetFunction: any) { return { value: function (...args: any[]) { let result; zoneService.zone.current.run(() => { zoneService.zone.reference = this.serializeId; result = targetFunction.value.apply(this, args); }); return result; } }; }
zoneService looks quite simple
export class ZoneService { get zone() { return (<any>window).Zone; } } export default new ZoneService();
Our business service
accountsService , which, using the implementation of the api service, receives data from the server:
class AccountsService { ... constructor(private apiService: ApiService) { } list(...): ICancelablePromise<AccountsDto> { return this.apiService.makeGetRequest<AccountsDto>(...); } ... }
Well, actually
apiService itself:
class ApiService { private requestsPool: Map<string, ICancelablePromise<any>[]> = new Map<string, ICancelablePromise<any>[]>(); ... public makeRequest<T>(...): ICancelablePromise<T> { const requestService = new HttpRequestService<T>(...); this.httpRequestConfigService.getConfig(...) .then(httpRequestConfig => requestService.doHttpRequest(httpRequestConfig)); this.addRequestToPool(zoneService.zone.reference, requestService.requestDeferred.promise); return requestService.requestDeferred.promise; } public cancelRequests(requestKey: string) { const requests = this.requestsPool.get(requestKey); if (requests) { _.forEach(requests, request => request.cancel()); this.requestsPool.delete(requestKey); } } ... private addRequestToPool(key: string, value: ICancelablePromise<any>) { const requests = this.requestsPool.get(key) || []; requests.push(value); this.requestsPool.set(key, requests); } }
In the api service, we write each request to the pool, with the key being the
zoneService.zone.reference , which was established in the zone at the highest level in the function call chain in the decorator.
... zoneService.zone.current.run(() => { zoneService.zone.reference = this.serializeId; result = targetFunction.value.apply(this, args); }); ...
By running a function in a zone, in any subsequent link in the call chain, we get the context of the call by simply receiving the zone. This solves the problem of contexts for deep call chains in one fell swoop.
A link to the component instance is available in the required location. Now in the
$ onDestroy method of the
BaseController base controller
, we simply call
... apiService.cancelRequests(this.serializeId); ...
and we get the desired result.
Do not be afraid of terrible beasts and new technologies.