Home > Net >  Javascript Class - Chaining async methods and returning "this"
Javascript Class - Chaining async methods and returning "this"

Time:10-09

I'm attempting to introduce a queue-type system to a JS class to allow for Async method chaining, ideally, I'd like to perform operations on the class instance using these async methods and return "this" instance.

export class Queue {
    constructor() {
        this.queue = Promise.resolve()
        this.firstRequestStarted = false
        this.firstRequestStatusCode  = 0
        this.secondRequestStarted = false
        this.secondRequestStatusCode = 0
    }
    then(callback) {
        callback(this.queue)
    }
    chain(callback) {
        return this.queue = this.queue.then(callback)
    }
    first() {
        this.chain(async () => {
            try {
                this.firstRequestStarted = true
                const response = await axios.get("https://stackoverflow.com/questions")
                this.firstRequestStatusCode = response.status
                return this
            }
            catch (e) {
                const { message = "" } = e || {}
                return Promise.reject({ message })
            }
        })
        return this
    }
    second() {
        this.chain(async () => {
            try {
                this.secondRequestStarted = true
                const response = await axios.get("https://stackoverflow.com/")
                this.secondRequestStatusCode = response.status
                return this
            }
            catch (e) {
                const { message = "" } = e || {}
                return Promise.reject({ message })
            }
        })
        return this
    }
}

Functions are added to the queue, and as we await them, the "then" method will handle their execution.

const x = await new Queue()
            .first()
            .second()
        console.log(x)

The challenge I'm facing is that I can never actually get "this" (instance of Queue) back to x.

1) x === undefined
2) "Chaining cycle detected for promise #<Promise>"
or ( I haven't been able to track down where this one is coming from, node error)
3) finished with exit code 130 (interrupted by signal 2: SIGINT)

I have tried adding a "consume" method, which simply returns "this", this leads to error #2 above

me() {
        this.chain( () => {
            try {
                return this
            }
            catch (e) {
                const { message = "" } = e || {}
                return Promise.reject({ message })
            }
        })
        return this
    }

The confusion on my part, is that if I use any value other than "this", it works as expected

me() {
        this.chain( () => {
            try {
                return "test"
            }
            catch (e) {
                const { message = "" } = e || {}
                return Promise.reject({ message })
            }
        })
        return this
    }

x === "test" I'm also able to return the values associated to this with something like the following return {...this}

Ideally, I'd like to return the instance of Queue to X, as I plan on modifying the properties of the Queue instance through my async methods, await them, and be returned with an "initialized" instance of Queue.

Any input would be greatly appreciated - thank you!

CodePudding user response:

The problem is that your Queue instances are thenable (have a .then() method), and the promise is tried to be resolved with itself (this.queue). See also here or there.

You have two options:

  • Do not resolve your promise with the instance, but write

    const x = new Queue().first().second();
    await x;
    console.log(x);
    
  • remove the then method from your class, then call

    const x = new Queue().first().second().queue;
    console.log(x);
    

    (or possibly introduce a getter method - .get(), .toPromise() - instead of directly accessing .queue)

CodePudding user response:

[ Warning, this answer contains some flaws as pointed out by Bergi, for example, run cannot be called multiple times, and it includes a race condition ]

Attached you can find a possible solution for this problem is to resolve all the promises sequentially.

Be aware, I did not include any error handling.

First, we define a Queue class

class Queue {
  responses = [];
  queue = [];

  async run() {
    for (const task of this.queue) await task(this);
    this.queue = [];
    return this;
  }

  chain(cb) {
    this.queue.push(cb);
    return this;
  }
}

This class allows us to use the chain function to add steps to the queue, and the resolve function will resolve them all sequentially.

Then, we can extend it to any class that needs it, for example, a fetch class

class QueueWithPromise extends Queue {
  actions = {
    first: async (self) => {
      const response = await fetch('https://stackoverflow.com/');
      self.responses.push(response);
      return;
    },
    second: async (self) => {
      const response = await fetch('https://stackoverflow.com/');
      self.responses.push(response);
      return;
    },
  };

  schedule(action) {
    if (this.actions[action]) this.chain(this.actions[action]);
    return this;
  }
}

and then we can chain any amount of promises to it and it will return the class after resolving each promise sequentially, make sure to add "resolve()" at the end =)

const response = await (new QueueWithPromise()).schedule('first').schedule('second').run()
console.log(response); //QueueWithPromise {responses: Array(2), queue: Array(0), actions: {…}}
  • Related