Ways to sequentially execute API calls rather than concurrently

Update: Find the complete solution at the end of this answer.


Consider the following code snippet:

@Injectable()
export class FileUploader {

    constructor(private http: Http) {}

    upload(url: string, file: File) {
        let fileReader: FileReader = new FileReader();

        return new Promise((resolve, reject) => {
            fileReader.onloadend = (e) => {
                // you can perform an action with read data here
                let content = fileReader.result;
                console.log('starting upload');
                return this.http.post(url, content)
                    .map((res) => res.json()).toPromise();
            };

            fileReader.readAsArrayBuffer(file);
        });
    }

and usage like

this.fileuploader.upload('/backend/upload', content).then(); // do something

However, if a user selects multiple files (like creating an album on Facebook), all files will be uploaded simultaneously, causing the browser to become completely unresponsive.

My idea was to store an array of promises in a property and have another private method trigger the first one. Once it is done, the promise would call that method again to start a new upload until all are completed.

I've tried various combinations without success, and I couldn't even get them to compile. The code above isn't mine; I found it in response to a different question.

How can I accomplish this task?


Edit: Following the advice from @toskv, I have implemented the solution below. I've updated my answer to help others facing the same issue.

Thank you once again to @toskv for the assistance.

@Injectable()
export class FileUploader {

    private currentTask: Promise<any> = null;

    constructor(private http: Http) {}

    upload(url: string, file: File) {
        let action = () => {
            return new Promise((resolve) => {
                let fileReader: FileReader = new FileReader();
                fileReader.onloadend = (e) => {
                    let content = fileReader.result;
                    return this.http.post(url, content)
                        .map((res) => res.json()).toPromise()
                        .then((json) => {
                            resolve(json);
                        });
                };

                fileReader.readAsArrayBuffer(file);
            })
        };

        return this.doNext(action)
    }

    private doNext(action: () => Promise<any>): Promise<any> {
        if (this.currentTask) {
            // if something is in progress do it after it is done
            this.currentTask = this.currentTask.then(action);
        } else {
            // if this is the only action do it now
            this.currentTask = action();
        }
        return this.currentTask;
    }
}

Answer №1

When enclosing a call within a function, it becomes easier to encapsulate them in the following manner.

function chain(calls: Array<() => Promise<any>>): Promise<any> {
   return calls.reduce((previous, current) => {
       return previous.then(current);
   }, Promise.resolve(""));
}

The purpose of this function is to iterate through the list of promises (which starts with an already resolved one created by Promise.resolve) and assign each function in the list of promises as the then callback for the resulting promise.

It can be illustrated like so:

Promise
  .resolve("")
  .then(first)
  .then(second);

To demonstrate how it can be utilized, consider the following example.

let first = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('first');
      resolve();
    }, 3000);
  })
}

let second = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('second');
      resolve();
    }, 100);
  })
}

chain([first, second]);

A functional live demo can be accessed here.

Another approach involves converting an array of items into functions that return promises, such as this.

let files = ['a', 'b', 'c'];

function uploadFile(file) {
  return Promise.resolve<any>('v');
}
let uploads = files.map((file) => () => uploadFile(file))
chain(uploads);

Based on our chat conversation, it appears you require a service capable of queuing incoming requests. Here's a basic TypeScript example that can easily integrate into your file upload service with the help of @Inject annotation.

class QueuedDispacherService {
  private currentTask: Promise<any> = null;

  constructor() { }

  doNext(action: () => Promise<any>) {
    if (this.currentTask) {
      this.currentTask = this.currentTask.then(action);
    } else {
      this.currentTask = action();
    }
    return this.currentTask;
  }
}


let dispatcher = new QueuedDispacherService();


let action1 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('first task done!');
      resolve('done 1!');
    }, 10000);
  })
}


let action2 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('second task done!');
      resolve('done 2!');
    }, 300);
  })
}

dispatcher.doNext(action1);
dispatcher.doNext(action2);

The same method of creating action functions as seen in the previous example can be applied here.

You can also view the working model here.

The ability to subscribe to each upload by utilizing the promise returned by the doNext method is also a valuable feature.

dispatcher.doNext(action1).then((result) => console.log(result));
dispatcher.doNext(action2).then((result) => console.log(result));

If the currentTask is initialized with a resolved promise, the code can be streamlined accordingly.

class QueuedDispacherService {
  private currentTask: Promise<any> = Promise.resolve();

  constructor() { }

  doNext(action: () => Promise<any>) {
    return this.currentTask = this.currentTask.then(action);
  }
}

This diagram showcases the runtime behavior, highlighting the capability of the Promise API to enhance the management of asynchronous operations.

https://i.stack.imgur.com/wTkf1.png

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is there a way to determine if a string is empty, even if it contains hard returns?

I am currently working on a function that checks if a string is empty or not, but it seems to be missing the detection of new lines. export const isStrEmpty = function(text: string): boolean { return !text || text.match(/^ *$/) !== null; }; I attempted ...

Encountering an issue while trying to launch an Angular application on localhost. Vendor.js resource failed to load

Whenever I compile the code, it runs successfully. However, when I try to serve the application using 'node --max-old-space-size=8192', even though it compiles without any errors, when I open the app in a browser, it returns an error saying "Cann ...

How to handle an empty data response in Angular 2's HTTP service

My ASP.NET web API has a simple method with the following test results: $ curl localhost:5000/Api/GetAllQuestions [{"questionId":0,"value":"qqq","answers":[{"answerId":25,"value":"qwerty"}]}] However, I am encountering an issue in my Angular 2 HTTP serv ...

The error message "Property 'hideKeyboardAccessoryBar' does not exist on type 'Keyboard'." appeared while using the IONIC Moodle App

Having an issue in the IONIC Moodle App with a typescript error stating that property 'hideKeyboardAccessoryBar' does not exist on type 'Keyboard'. An ionic error occurred when running CMD, displaying the following error: [14:58:02] ...

Associate a unique identifier string with a randomly generated integer identifier by Agora

For my current web project, I am utilizing a String username as the UID to connect to the channel in an Agora video call. However, I now need to incorporate individual cloud recording by Agora into the project. The challenge lies in the fact that cloud r ...

Ensure the JSON file aligns with the TypeScript Interface

I am working with a config.json file. { "profiler": { "port": 8001, "profilerCache": { "allowedOriginsRegex": ["^http:\/\/localhost:8080$", "i"] } }, "database": { "uri": "mongodb+srv://...", "dbName": "profiler", ...

Endpoint not returning data as expected

I'm facing an issue with my routing module where I have successfully used activatedRoute for multiple other modules but now running into trouble when implementing it in a new singular component. The structure of my routing module is as follows: const ...

Inform the Angular2 Component regarding the creation of DOM elements that are placed outside of the

The Challenge In my Angular2 project, I am using Swiper carousel and building it with Webpack. However, Angular2 adds random attributes like _ngcontent-pmm-6 to all elements in a component. Swiper generates pagination elements dynamically, outside of Ang ...

Guide on how to display matching options in an input box using HTML datalist based on user input at the beginning

I am a beginner in React and I am looking to create an autocomplete suggestion box for a text input field. I want the suggestions to only match the first letters of the available options, unlike the current behavior of HTML's <datalist>. Althou ...

Expanding function parameter types using intersection type in Typescript

As I delve into the world of intersection types to enhance a function with an incomplete definition, I encountered an interesting scenario. Take a look at this code snippet: WebApp.connectHandlers.use("/route", (req:IncomingMessage, res:ServerResponse)=& ...

Error: Class cannot be loaded by React Webpack loader

I'm encountering an issue with my two typescript packages - a React application and an infrastructure package. The React app has a dependency on the infrastructure package (currently linked via npm). After adding a new class to the infrastructure pack ...

Issue with Async pipe when utilizing autocomplete functionality

HTML Code <mat-form-field> <input type="text" matInput class="formControl" [formControl]="name" [matAutocomplete]="auto" > <mat-autocomplete #auto="matAutocomplete"> <mat-option *ngFor="let option of city | async" [valu ...

I'm running into issues transferring data between Angular and an API

While working on an Angular project, I encountered an issue where I couldn't populate data from an API into a table. I suspected there was an error in the datasource section but couldn't pinpoint it. When checking the console, I verified that the ...

Angular2: AuthGuard malfunctioning with browser navigation controls

My AuthGuard setup works flawlessly during normal navigation within the application (see code below). Now, consider this scenario: A user goes to /content/secured-content, which requires authentication => they are redirected to /authentication/login d ...

ngx-scroll-event activated, yet remains elusive

After updating my project from Angular 7 to 8 smoothly, I proceeded with the update to Angular 9. Suddenly, the project was unable to find the required [email protected] package, resulting in a "module not found" error: Cannot find module 'ngx-sc ...

Issue with TypeScript Declaration File in NPM module functionality

Recently, I've been working on developing a package for NPM. It's essentially a JSON wrapped database concept, and it has been quite an enjoyable project so far. However, I've been facing some challenges when trying to include declarations f ...

Eliminating Body Tag Margin in Angular 4: A Step-by-Step Guide

In the Angular 4 project, the app.component.html file does not include a body tag that can be styled to eliminate the padding associated with the body tag. An attempt was made in the app.component.css file to remove the margin with the following code, but ...

What is the process of converting a byte array into a blob using JavaScript specifically for Angular?

When I receive an excel file from the backend as a byte array, my goal is to convert it into a blob and then save it as a file. Below is the code snippet that demonstrates how I achieve this: this.getFile().subscribe((response) => { const byteArra ...

The index type '{id:number, name:string}' is not compatible for use

I am attempting to generate mock data using a custom model type that I have created. Model export class CategoryModel { /** * Properties */ public id : number; public name : string; /** * Getters */ get Id():number{ return this.id; ...

Executing functions in Vue TypeScript during initialization, creation, or mounting stages

Just a few hours ago, I kicked off my Vue TypeScript project. I've successfully configured eslint and tslint rules to format the code as desired, which has left me quite pleased. Now, I'm curious about how to utilize the created/mounted lifecycl ...