JorgeeFG
JorgeeFG

Reputation: 5971

How to make chainable async functions work in parallel, then wait only for last function?

I want to do the following:

const contextSeeder = new ContextSeeder(context);
let props = (await (await contextSeeder.withList()).withUser()).get();

But ideally this would look like:

const contextSeeder = new ContextSeeder(context);
let props = await contextSeeder.withList().withUser().get();

withList() and withUser() will both make an API call. So ideally they should run in parallel. They both modify an object in the class which is the return value (could there be a race condition?) So given that I call the two "loaders", I'd want the .get() to be the one that must actually be waited for. Is it possible? Because in the first way it looks like it will handle one by one (because of the await in both calls)

I run this inside an async function, and I need this data to be present by the time the function returns. I can't do a .then()

Thanks

export default class ContextSeeder {

    context = {};
    serverContext;

    constructor(serverContext = undefined) {
        this.serverContext = serverContext;
    }

    withUser = async (listId = undefined) => {
        let userContext = {};
        await UserContextSeeder(this.serverContext).then(context => {userContext = context});
        this.context = {...this.context, user: userContext}
        return this;
    }

    withList = async (userId = undefined) => {
        let listContext = {};
        await ListContextSeeder(context).then(context => {listContext = context});
        this.context = {...this.context, list: listContext}
        return this;
    }

    get = () => {
        return this.context;
    }
}

Upvotes: 0

Views: 73

Answers (4)

JorgeeFG
JorgeeFG

Reputation: 5971

I made a different approach, based on the answers I got here and Any difference between await Promise.all() and multiple await?

I did not want to await for every call independently, because that would make them to be serialized instead of parallel.

So I made sure to remove all the await and leave only one:

export async function getServerSideProps(context) {
    const contextSeeder = new ContextSeeder(context);
    let props = await contextSeeder.withList(335030).withUser().get();

    return {
        props: props,
    }
}

This way I got what I wanted, and performance wise it is better.

ContextSeeder.js

import UserContextSeeder from "./UserContextSeeder";
import ListContextSeeder from "./ListContextSeeder";

/**
 *
 */
export default class ContextSeeder {

    returnContext = {};
    serverContext;

    withListEnabled = false;
    listId;

    withUserEnabled = false;
    userId;

    constructor(serverContext = undefined) {
        this.serverContext = serverContext;
    }

    withList = (listId = undefined) => {
        this.withListEnabled = true;
        this.listId = listId;
        return this;
    }

    withUser = (userId = undefined) => {
        this.withUserEnabled = true;
        this.userId = userId;
        return this;
    }

    _withList = async () => {
        return ListContextSeeder(this.listId)
            .then(response => response.json())
            .then(listData => {this.returnContext.list = listData});
    }

    _withUser = async (userId) => {
        return UserContextSeeder(this.serverContext, userId)
            .then(response => response.json())
            .then(userData => {this.returnContext.user = userData});
    }

    get = async () => {
        let promises = [];

        if (this.withListEnabled) {
            promises.push(this._withList(this.listId));
        }

        if (this.withUserEnabled) {
            promises.push(this._withUser(this.userId));
        }

        await Promise.all(promises.map(p => p.catch(e => e)))
            .then(results => console.log(results))
            .catch(e => console.log(e));

        return this.returnContext;
    }
}

ListContentSeeder.js

export default async function ListContextSeeder(listId) {
    const listManager = new ListManager();

    return listManager.getList(listId);
}

ListManager.getList(listId) simply returns an isomorphic-unfetch promise.

Upvotes: 0

Ben Aston
Ben Aston

Reputation: 55749

The following kicks-off the async retrievals one by one, and then waits for them to complete in parallel using Promise.all. Finally, the results are destructured and then added to an object to be returned.

I think this is a simplification of your code, because intermediate state is not shared publicly.

I would rename ListContextSeeder and UserContextSeeder to be something more idiomatic. Capitalized names are usually reserved for constructor functions.

export default async function seedContext({ serverContext, listId, userId }) {
  const p1 = ListContextSeeder(serverContext, listId)
  const p2 = UserContextSeeder(serverContext, userId)
  const [list, user] = await Promise.all([p1, p2])
  return { list, user }
}

Upvotes: 1

Elias Faraone
Elias Faraone

Reputation: 286

For the purpose of this answer, I'll ignore the fact that you're not using userId nor listId.
You could simplify your code with the following:

export default class ContextSeeder {
    context = {};
    serverContext;

    constructor(serverContext = undefined) {
        this.serverContext = serverContext;
    }

    withList = async (listId = undefined) => {
        let seededUserContext = await UserContextSeeder(this.serverContext);

        this.context = {...this.context, user: seededUserContext}

        // Not needed unless you still want to chain them
        // return this;
    }

    withUser = async (userId = undefined) => {
        let seededListContext = await ListContextSeeder(context);

        this.context = {...this.context, list: seededListContext}

        // Not needed unless you still want to chain them
        // return this;
    }

    get = () => {
        return this.context;
    }
}

Then, you can await the resolution of both by using Promise.all():

// They now run in "parallel"
await Promise.all([contextSeeder.withList(), contextSeeder.withUser());
let props = contexSeeder.get();

Update

Based on Ben Aston's answer below, another improvement would be to encapsulate this behaviour in another internal method, pass an intermediate context and only set the actual context once both the calls are done.

export default class ContextSeeder {
  context = {};
  serverContext;

  constructor(serverContext = undefined) {
    this.serverContext = serverContext;
  }

  withList = async (listId = undefined, intermediateContext) => {
    let seededUserContext = await UserContextSeeder(this.serverContext);

    // No spread operator needed, just set the property
    intermediateContext.user = seededUserContext;

    // Not needed unless you still want to chain them
    // return this;
  };

  withUser = async (userId = undefined, intermediateContext) => {
    let seededListContext = await ListContextSeeder(context);

    // No spread operator needed, just set the property
    intermediateContext.list = seededListContext;

    // Not needed unless you still want to chain them
    // return this;
  };

  seedContext = async (userId, listId) => {
    let intermediateContext = {};

    await Promise.all([
      withUser(userId, intermediateContext),
      withList(listId, intermediateContext)
    ]);

    this.context = intermediateContext;
  }

  get = () => {
    return this.context;
  };
}

Then, simply await seedContext:

await contextSeeder.seedContext(userId, listId);
let props = contextSeeder.get();

Upvotes: 1

Aplet123
Aplet123

Reputation: 35522

If you want to run the requests in parallel, then wait for them both to finish before calling .get(), you can try the following code using Promise.all:

// we wait for both promises to finish
await Promise.all([contextSeeder.withList(), contextSeeder.withUser()]);
// they're both finished so we can get it now
let props = contextSeeder.get();

Upvotes: 1

Related Questions