Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(httpbackend): ability to cancel Http requests #914

Merged
merged 5 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions docs/cancel_http_requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: Cancel HTTP requests
author: Roxane Letourneau
---

Having Taquito implemented in composable modules is a design choice to allow users to customize the modules to meet some of their specific needs.

One of these needs might be the ability to cancel HTTP requests to optimize the network. Indeed, Taquito has heavy methods that make a lot of requests to the RPC. For example, in some cases, users might want to cancel almost immediately a call when using it in user interfaces. It is possible to incorporate some logic into the `HttpBackend` and `RpcClient` classes to fulfill this need.

Here is an example in which we can click the `cancel` button during an estimation call to abort all requests. It will throw an exception.

```js live noInline abort
const amount = 2;
const address = 'tz1h3rQ8wBxFd8L9B3d7Jhaawu6Z568XU3xY';

println(`Estimating the transfer of ${amount} ꜩ to ${address} : `);
Tezos.estimate
.transfer({ to: address, amount: amount })
.then((est) => {
println(`burnFeeMutez : ${est.burnFeeMutez},
gasLimit : ${est.gasLimit},
minimalFeeMutez : ${est.minimalFeeMutez},
storageLimit : ${est.storageLimit},
suggestedFeeMutez : ${est.suggestedFeeMutez},
totalCost : ${est.totalCost},
usingBaseFeeMutez : ${est.usingBaseFeeMutez}`);
})
.catch((error) => println(`Error: ${JSON.stringify(error, null, 2)}`));
```

Here are the steps that we implemented to built the precedent example:

1. Create a custom `HttpBackend`
We created a class called `CancellableHttpBackend` which extended the `HttpBackend` class, and we overrode the `createRequest` method. We used the [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to help to abort the fetch requests. We added logic to the `createRequest` method to handle the abort signal.

``` ts
import { HttpBackend, HttpRequestFailed, HttpResponseError, STATUS_CODE, HttpRequestOptions } from '@taquito/http-utils';
import AbortController from "abort-controller";

class CancellableHttpBackend extends HttpBackend {
private abortCtrl: AbortController;
constructor(){
super();
this.abortCtrl = new AbortController();
}

cancelRequest(){
this.abortCtrl.abort();
};

createRequest<T>(
{ url, method, timeout, query, headers = {}, json = true, mimeType = undefined }: HttpRequestOptions,
data?: {}
) {
return new Promise<T>((resolve, reject) => {

[...]

request.onabort = function () {
reject(
new HttpResponseError(
`Request canceled`,
this.status as STATUS_CODE,
request.statusText,
request.response,
url
)
);
};

const abort = () => {
request.abort();
}

this.abortCtrl.signal.addEventListener("abort", abort);

[...]
});
}
}
```

2. Create a custom `RpcClient`
We created a class called `CancellableRpcClient` which extends the `RpcClient` class. We passed to its constructor an instance of our `CancellableHttpBackend` class. We also added a `cancelRequest` method which is used to trigger the abort signal.

``` ts
import { RpcClient } from '@taquito/rpc';

class CancellableRpcClient extends RpcClient {
httpBackend: CancellableHttpBackend;

constructor(
url: string,
chain: string = 'main',
customHttpBackend: CancellableHttpBackend = new CancellableHttpBackend()
) {
super(url, chain, customHttpBackend),
this.httpBackend = customHttpBackend;
}

cancelRequest(){
this.httpBackend.cancelRequest();
}
}
```
3. Set the RpcProvider
Then, we set our `CancellableRpcClient` on our `TezosToolkit` instance instead of using the default `RpcClient` class:

``` ts
import { TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';

const signer: any = new InMemorySigner('your_key');
const customRpcClient = new CancellableRpcClient('your_RPC_URL')
const tezos = new TezosToolkit(customRpcClient);
tezos.setSignerProvider(signer);
```

4. Trigger the abort signal
We linked the `cancelRequest` method of the `CancellableRpcClient` class to a `cancel` button. The initiator of the abort signal might be different based on your use cases. Note that the cancelation action is not specific to a method in the example, meaning that all RPC calls will be aborted.

``` ts
Tezos.rpc.cancelRequest();
```

26 changes: 17 additions & 9 deletions packages/taquito-http-utils/src/taquito-http-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export { VERSION } from './version';

const defaultTimeout = 30000;

interface HttpRequestOptions {
export interface HttpRequestOptions {
url: string;
method?: 'GET' | 'POST';
timeout?: number;
Expand Down Expand Up @@ -49,7 +49,7 @@ export class HttpRequestFailed implements Error {
}

export class HttpBackend {
private serialize(obj?: { [key: string]: any }) {
protected serialize(obj?: { [key: string]: any }) {
if (!obj) {
return '';
}
Expand All @@ -67,7 +67,7 @@ export class HttpBackend {
// another use case is multiple arguments with the same name
// they are passed as array
if (Array.isArray(prop)) {
prop.forEach(item => {
prop.forEach((item) => {
str.push(encodeURIComponent(p) + '=' + encodeURIComponent(item));
});
continue;
Expand All @@ -83,7 +83,7 @@ export class HttpBackend {
}
}

private createXHR(): XMLHttpRequest {
protected createXHR(): XMLHttpRequest {
return new XMLHttpRequestCTOR();
}

Expand All @@ -92,7 +92,15 @@ export class HttpBackend {
* @param options contains options to be passed for the HTTP request (url, method and timeout)
*/
createRequest<T>(
{ url, method, timeout, query, headers = {}, json = true, mimeType = undefined}: HttpRequestOptions,
{
url,
method,
timeout,
query,
headers = {},
json = true,
mimeType = undefined,
}: HttpRequestOptions,
data?: {}
) {
return new Promise<T>((resolve, reject) => {
Expand All @@ -101,14 +109,14 @@ export class HttpBackend {
if (!headers['Content-Type']) {
request.setRequestHeader('Content-Type', 'application/json');
}
if (mimeType){
if (mimeType) {
request.overrideMimeType(`${mimeType}`);
}
for (const k in headers) {
request.setRequestHeader(k, headers[k]);
}
request.timeout = timeout || defaultTimeout;
request.onload = function() {
request.onload = function () {
if (this.status >= 200 && this.status < 300) {
if (json) {
try {
Expand All @@ -132,11 +140,11 @@ export class HttpBackend {
}
};

request.ontimeout = function() {
request.ontimeout = function () {
reject(new Error(`Request timed out after: ${request.timeout}ms`));
};

request.onerror = function(err) {
request.onerror = function (err) {
reject(new HttpRequestFailed(url, err));
};

Expand Down
Loading