4

I am trying to figure out how to use useFactory as an async function in Angular 11. Right now I have this:

import { ApolloClientOptions } from 'apollo-client';
import { FirebaseService } from './firebase.service';
// other imports here...

export async function createApollo(httpLink: HttpLink, fs: FirebaseService): Promise<ApolloClientOptions<any>> {

  const token = await fs.getToken();

  const getHeaders = async () => {
    return {
      "X-Auth-Token": token,
    };
  };

  // functions has more content here...

  return Promise.resolve({
    link: errorLink.concat(link),
    cache: new InMemoryCache(),
  });
}

@NgModule({
  imports: [HttpLinkModule],
  providers: [
    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, FirebaseService],
    },
  ],
})
export class DgraphModule { }

The problem is that it resolves the async function, but not before returning it. I see some other posts on StackOverFlow to solve this problem, but they ultimately leave out the async part of the function. You cannot have an await that is not in an async function, so I am oblivious here.

You can also not put an async function within an async function without that function being an async function... so what do I do here?

UPDATE: 2/11/21 - According to this there is a way to do this with PlatformBrowserDynamic(), but I am not understanding how to implement it in my module.

UPDATE: 2/13/21 Here is the link to codeandbox.io - Ignore the lack of html or working endpoint, but you can view the module and change it to an async function to see there is an Invariant Violation error. ---> Make sure to view the codeandsandbox console for errors.

Jonathan
  • 3,893
  • 5
  • 46
  • 77
  • This function returns a promise, so inside the implementation you should be returning a promise. Within that promise you can then include your logic that should then resolve the promise at the end. So something like return new Promise(async function (resolve) { #some stuff here you await then call resolve("answer"'); }) – allan Feb 08 '21 at 11:05
  • I just returned a resolved Promise and I get the same error `Invariant Violation: To initialize Apollo Client, you must specify a 'cache' property in the options object.` because it is returning the promise instead of the value. Everything works without async, but I need async for my token headers to work... See updated full code... – Jonathan Feb 08 '21 at 15:31
  • I'm not super familiar with apollo client. How do you expect the provider APOLLO_OPTIONS to be used? Should it just be an POJO js object? – g0rb Feb 12 '21 at 17:38
  • In this case it is just the options for Apollo (It could be anything but I updated the code with the import), but the point is how to use **async** with ```useFactory```. – Jonathan Feb 13 '21 at 15:03

2 Answers2

12

You cannot have an async factory function in the dependency injection it is not supported. There is a built in functionality that can help you workaround this issue called application initializers. You can register an application initializer in the dependency injection using the built in APP_INITIALIZER token. This token requires a factory that will return a function that will be called as the application initializer and it can return a promise. This application initializers are called at the start of the application and the application does not start until all finish. For your case I think this is ok since you read some kind of configuration. Unfortunately there is no good official documentation on this that I'm aware of. If you search you will find some articles on this topic. Here is a link to another stack overflow question that explains it. To use this approach in your case I think it is best to create a service that does the initialization and then holds the value.

@Injectable()
export class ApolloOptionsService {
  public apolloOptions: any;

  constructor(private httpLink: HttpLink) {}

  public createApollo() {
    const token = "";

    const getHeaders = async () => {
      return {
        "X-Auth-Token": token
      };
    };

    const http = ApolloLink.from([
      setContext(async () => {
        return {
          headers: await getHeaders()
        };
      }),
      this.httpLink.create({
        uri: `https://${endpoint}`
      })
    ]);

    // Create a WebSocket link:
    const ws = new WebSocketLink({
      uri: `wss://${endpoint}`,
      options: {
        reconnect: true,
        connectionParams: async () => {
          return await getHeaders();
        }
      }
    });

    const link = split(
      // split based on operation type
      ({ query }) => {
        const definition = getMainDefinition(query);
        return (
          definition.kind === "OperationDefinition" &&
          definition.operation === "subscription"
        );
      },
      ws,
      http
    );
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          link: link,
          cache: new InMemoryCache()
        });
      }, 5000);
    }).then((apoloOptions) => (this.apolloOptions = apoloOptions));
  }
}

As you can see I added also a delay in the promise to simulate that it finishes later. Now lets change also your module to use an app initializer.

export function createApollo(apolloOptionsService: ApolloOptionsService) {
  return () => apolloOptionsService.createApollo();
}

export function getApolloOptions(apolloOptionsService: ApolloOptionsService) {
  return apolloOptionsService.apolloOptions;
}

@NgModule({
  imports: [HttpLinkModule],
  providers: [
    ApolloOptionsService,
    {
      provide: APP_INITIALIZER,
      useFactory: createApollo,
      deps: [ApolloOptionsService],
      multi: true
    },
    {
      provide: APOLLO_OPTIONS,
      useFactory: getApolloOptions,
      deps: [ApolloOptionsService]
    }
  ]
})
export class GraphQLModule {}

As you can see we use one factory function to create the app initializer function that is called when the application initializes. The other factory function is needed to read the initialized value from the service and provide it as the APOLLO_OPTIONS token. Because the initializers are run before the application starts the value is ready before it gets used.

I also created a fork of your codesandbox where you can see this in action.

Aleš Doganoc
  • 11,568
  • 24
  • 40
0

So,

My specific use case here was to get a JSON file containing environment key-value pairs, all of which currently result in new ApolloLinks where appropriate. Right now, we have one for a "terminating" link and two for subscriptions. The reason we don't just use the environment files Angular provides is because I wanted to be able to set these values in our CI process, meaning we would facilitate that input once and then could build anywhere. In all this change took me one to two hours and we immediately scaled to 4 environments this way.

Ever since I introduced this way of setting variables, however, I needed some way to block the GraphQL client from being set up before they were known. So before now, the only way to do that was to fetch the JSON file in the main.ts file, save the JSON response to the window and then bootstrap the application in the same .then callback.

Then in the apollo-angular setup, I access the global variable on the window, read out the values I want, and then delete the global variable from the window right before returning out of the function. I imported the ApolloModule and then useFactory-d into the APOLLO_OPTIONS token.

I've been messing around with this setup on and off for three days. I tried the accepted answer above, but consistently got either "client has not been defined yet" errors, or an error about missing a cache property, which you get when you don't set the APOLLO_OPTIONS token at all. Only today did I actually find a way around this: it is possible to async this process.

To start, do not import the ApolloModule. This module requires the APOLLO_OPTIONS token, and you can't set this token value asynchronously.

Next, instead of providing APOLLO_OPTIONS, provide APP_INITIALIZER instead. Set both HttpLink and Apollo as its dependencies. Call useFactory to a function you declare within the same file.

From this function, call your async setup function. Then chain a .then into apollo.create(), passing the returned config into the .create() method.

Your async setup function is just that: an async function in which you call await fetch(), then await .json(), and then setup all your links, cache, and options. Return your final config object so that you can chain it into apollo.create(). Hurray, it works!

Crude code example, I'm not able to share the full source:

import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Apollo } from 'apollo-angular';

const setup = async (httpLink: HttpLink) => {
  const response = await fetch('');
  const data = await response.json();
  // rest of your setup
  return { links, cache, defaultOptions };
}

const factoryFn = (httpLink: HttpLink, apollo: Apollo) =>
   setup(httpLink)
       .then(config => apollo.create(config))
       .catch(e => console.error(e));

@NgModule({
    imports: [HttpClientModule],
    providers: [
    Apollo,
    {
        provide: APP_INITIALIZER, 
        useFactory: factoryFn, 
        deps: [HttpLink, Apollo]
    }
  ]
})
export class ModuleNameHere {}

So the long and short of it:

If you want to set up your Apollo client asynchronously, do not use APOLLO_OPTIONS. It always runs synchronously and APP_INITIALIZER will not block it.

Jorge
  • 41
  • 3