Search code examples
angularasync-awaitangular-moduleangular-providersangular11

angular useFactory return async Function in module


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.


Solution

  • 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.