Marmicode
Blog Post
Younes Jaaidi

End-to-End HTTP request cancelation with RxJS & NestJS

by Younes Jaaidi ā€¢ 
Feb 20, 2020 ā€¢ 6 minutes
End-to-End HTTP request cancelation with RxJS & NestJS

Life is too short. When searching for something, we canā€™t afford to type a whole word or sentence in a search field, or filling all the fields then hitting our old keyboardā€™s half-broken enter key to finally be able to see the first results... or nothing at all because our search criteria were too restrictive.

Donā€™t look at me like that! We can probably agree that most of us, if not all, are . We get frustrated every time we have to submit a search form.

TL;DR:

šŸšØ Reactive Programming & RxJS to the rescue

Implementing these kinds of features can be tricky, especially if developed from scratch and with an imperative approach. Thatā€™s when reactive programming and RxJS come to the rescue. In fact, RxJS provides the right tooling and operators to implement these features in a few lines. RxJS is such a perfect fit for these scenarios that most courses and tutorials cover the live search topic. It helps understand both how reactive programming works and how it can easily solve some challenging issues.

Thatā€™s when we end up with this common recipe:

keywords$ = this.keywordsControl.valueChanges;
data$ = keywords$.pipe(
  /* Wait for the user to stop typing for 100ms and emit last value. */
  debounceTime(100),
  /* Ignore identical successive values
   * (e.g. user pastes the same value in the input). */
  distinctUntilChanged(), 
  /* when new keywords are emitted, this unsubscribes from the previous
   * search result (canceling the underlying http request)
   * and subscribes to the new one. */
  switchMap(keywords => this.search(keywords))
)

The illustration below might help you notice the difference between and the related operators:

RxJS Flattening Strategies

but if it doesn't help, you should definitely check out the great work by my buddy Shai Reznik:

šŸ¢ Hey Debounce! Stop bullying my low latency!

The problem is that you are probably investing a lot of energy and money in producing low latency architectures and APIs but all these

What if we just get rid of the debounce? We are using after all, and unnecessary requests are immediately canceled.

Request cancelation

Wait a second! What happens on the back-end? Is the back-end "work" interrupted by some voodoo magic? Or where the back-end is working for nothing until it realizes that the consumer is not there anymore?

šŸˆ Here comes the big cat

In a few words, NestJS is .

Amongst its wealth of features, there is . This is quite handy even if we respond with a single value and not a stream of values. In fact, .

šŸ§Ø Observables Teardown Logic

Observables are said because we can unsubscribe whenever we need to, and interrupt the work. Cancelation works thanks to the teardown logic .

Hereā€™s an example of wrapping setInterval in an observable:

function interval(period) {
  return new Observable(observer => {
    let i = 0;
    const handle = setInterval(() => observer.next(i++), period);
    /* This is the teardown logic. */
    return () => clearInterval(handle);
  });
}

As you can see, the observer function given to the Observable's constructor returns the in order to cancel the tasks scheduled by setInterval.

āš ļø This implementation is naive. You should use or instead.

šŸ§Ŗ The experiment

For the experiment, I needed to run some slow CPU, filesystem and memory intensive work on the back-end for every request. The first idea that crossed my mind was reading a big text file line by line and matching every one of them against the given keywords. It turned out that even with a 1GB file, it was still quite fast.

Thatā€™s when I thought that . I just needed to generate a directory with lots of files... but wait! šŸ¤”

Bingo! It could not be worse and that is exactly what I needed.

The implementation looks something like this and as you can see, the .

function getFiles(directoryPath) {
  return new Observable(observer => {
    ...
    return () => walker.pause();
  }
}

function readLines(filePath) {
  return new Observable(observer => {
    ...
    return () => reader.close();
  }
}

function search(): Observable<Line[]> {
  return getFiles(nodeModulesPath)
    .pipe(
      mergeMap(file => readLines(file)),
      ...
    );
}

šŸ˜” The disappointment

In the animation below, we can observe high CPU usage and an exponential memory usage on the back-end and that .

CPU &amp; Memory Usage

By diving a little bit in , we can see that In fact, Nest has to adapt to frameworks like ExpressJS that donā€™t handle observables.

  public async transformToResult(resultOrDeferred: any) {
    if (resultOrDeferred && isFunction(resultOrDeferred.subscribe)) {
      return resultOrDeferred.toPromise();
    }
    return resultOrDeferred;
  }

šŸ” Detecting request cancelation

In Nest, request objects are instances of NodeJSā€™ that .

If we can detect when the request is canceled, then we can interrupt the work in our RxJS response stream.

Nest has an interesting concept called :

and it looks like this:

@Injectable()
export class NoopInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle();
  }
}

This makes it possible to write, , a function that:

NestJS interceptors are Observable friendly

One of the interesting properties of Nest interceptors, compared to Express middlewares for example, is that the next parameter is not just a function that triggers the route function or the next middleware but .

Thanks to this feature, .

For instance, we can using RxJS's and using the operator.

The final interceptor should look like this:

@Injectable()
export class UnsubscribeOnCloseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    if (context.getType() !== 'http') {
      return next.handle();
    }

    const request = context.switchToHttp().getRequest() as Request;

    const close$ = fromEvent(request, 'close');

    return next.handle().pipe(takeUntil(close$));
  }
}

Let's try it out!

CPU &amp; memory usage with interceptor

As you can observe, thanks to the interceptor, . This reduces CPU, memory and all resources usage and interrupts all the work even when the user simply closes the window.

šŸ§  Think reactive

The key takeaway here is that with a generic interceptor.

MongoDB query cancelation

What if our data source was a database like MongoDB? Can we interrupt the query?

šŸ”— Links

šŸ’» Nx monorepo with an Angular app, a NestJS API and custom CPU / Memory graphing app using Angular & GraphQL subscriptions.