3

I'm making multiple concurrent DELETE requests to an API, causing a DeadLock on the server side.

If I had a fixed number of deletions I could do something like this:

myAPI.delete(id).subscribe(res => {
   myAPI.delete(id2).subscribe(res => {
      myAPI.delete(id3).subscribe(res => {
      })
   })
})

Waiting to the previous deletion to be succesfully completed before making a new one (secuential).

The problem is that I can have from 1 to N requests, so I'm looking for something similar to a for instruction on subscription.

Is there any elegant approach for solving this?

5
  • 1
    Can you try a recursive function? Commented Aug 24, 2020 at 10:43
  • 2
    Why would you send 1 to n requests but rather sending all 1...N parameters to serverand handle it there Commented Aug 24, 2020 at 11:09
  • 1
    @BasheerKharoti you are right, but its an external API that I have no control over Commented Aug 24, 2020 at 11:14
  • 3
    This is a poor API design. You should be doing this server side. Commented Aug 24, 2020 at 11:23
  • 1
    @Iñigo agreed with Adrian Brand you should pass all IDs to server and then perform all delete operations there instead of making multiple delete calls from front end. Commented Aug 24, 2020 at 11:33

4 Answers 4

4

If you have an array of ids that need to be removed, you can use the from operator in combination with concatMap and toArray. The last one is to only call the subscribe listener after all have been completed:

const ids = [ 1, 2, 3 ];

from(ids).pipe(
  concatMap((id) => deleteThis(id)),
  toArray()
).subscribe(() => {
   console.log('all deleted');
})

working stack with mock request observable

Sign up to request clarification or add additional context in comments.

Comments

3

There are multiple methods.

Sequential requests

You could use the RxJS range operator with mapping operator concatMap to call each request sequentially.

import { range } from 'rxjs';
import { concatMap } from 'rxjs';

range(1, N).pipe(                      // <-- will emit 1-N numbers
  concatMap(id => myAPI.delete(id))    // <-- set the correct `id` for each request here
).subscribe(
  res => console.log(res),
  err => console.log(err),
);

Since we're using concatMap operator, the request for each id will wait before the previous request is complete.

You could also use other mapping operators like switchMap, flatMap and exhaustMap for different behaviors. Difference b/n them here.


Other methods - Parallel requests

Complete parallel requests

You could make multiple concurrent calls using the RxJS toArray and forkJoin() operators. It subscribes to multiple observables simultaneously.

import { range, forkJoin } from 'rxjs';
import { concatMap, toArray } from 'rxjs/operators';

range(1, N).pipe(                      // <-- will emit 1-N numbers
  concatMap(id => myAPI.delete(id)),   // <-- set the correct `id` for each request here
  toArray(),                           // <-- buffer all notifications and emit once as an array
  concatMap(reqs => forkJoin(reqs))
).subscribe(
  res => console.log(res),
  err => console.log(err),
);

Buffered parallel requests

Most browsers have a hard limit on number of maximum parallel requests to a single domain (eg. Chrome - 6). If you run into this issue you could use RxJS bufferCount operator instead of the toArray operator to control the max number of parallel requests.

import { from, forkJoin } from 'rxjs';
import { concatMap, bufferCount } from 'rxjs';

range(1, N).pipe(                      // <-- will emit 1-N numbers
  concatMap(id => myAPI.delete(id)),   // <-- set the correct `id` for each request here
  bufferCount(6),                      // <-- adjust number of parallel requests here
  concatMap(reqs => forkJoin(reqs))
).subscribe(
  res => console.log(res),
  err => console.log(err),
);

4 Comments

But if they are simultaneous requests the server will be overloaded anyway, not solving the deadlocking issue, right? I was trying to make the requests sequential
@Iñigo: I've updated the answer to include complete sequential requests.
I advise editing this answer to only include the concatMap solution, as it is the only one which provides the requested behaviour, i.e. sequential requests
@WillAlexander: Good point, but instead of outright removing the other answers, I've bumped the sequential solution to the top and other solutions to the bottom. This way people have access to other solutions as well.
3

Alternative solution is to use concat operator:

import { concat } from 'rxjs';
import { toArray } from 'rxjs/operators';

concat(
   ...[1, 2, 3].map(id => this.delete(id)),
)
.pipe(toArray())
.subscribe(() => console.log('all deleted'))

or without toArray() operator:

import { concat } from 'rxjs';

concat(...[1, 2, 3].map(id => this.delete(id)))
  .subscribe(
    res => {
      console.log('one deleted')
    },
    err => {},
    () => {
      console.log('all deleted')
    })

1 Comment

don't forget the toArray() :)
0
const N = 10;
range(1, N)
  .pipe(concatMap(deleteById), ignoreElements())
  .subscribe({
      complete: () => console.log('Complete')
  });

function deleteById(id: number): Observable<String> {
  return of(`deleted-${id}`).pipe(delay(1000));
}

If you want make multiple request same time:


const N = 10;
const CHUNKED = 3; // (3 requests same time) concat (3 requests same time) ...

range(1, N)
  .pipe(
    bufferCount(CHUNKED),
    concatMap((ids) => forkJoin(ids.map(deleteById))),
    ignoreElements()
  )
  .subscribe({
    complete: () => console.log(`Complete`)
  });

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.