Fun with Angular: Create grouped queues of tasks for APP_INITIALIZER

Maxime - Jun 28 '23 - - Dev Community

Intro

Have you ever heard of APP_INITIALIZER? It's a handy DI token that lets you provide one or more initialization functions that'll be run before your entire app starts. They can come in very handy. But did you know that all the APP_INITIALIZER registered providers will be created at the same time?

Let's make that more flexible.

Because we desperately need it? Hell no.

Because I realized that limitation today and wanted to have some fun? Definitely.

It's a really good example to practice with dependency injection and observables. What else do we need for fun?

'What else'

APP_INITIALIZER and its limitations

If you want to make sure to execute an initialization function that is blocking before your app starts, APP_INITIALIZER is exactly what you need. Example from the documentation:



function initializeApp(): Promise<any> {
  return new Promise((resolve, reject) => {
    // Do some asynchronous stuff
    resolve();
  });
}

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: () => initializeApp,
      multi: true,
    },
  ],
})
export class AppModule {}


Enter fullscreen mode Exit fullscreen mode

Pretty cool, right?

But what happens you now have multiple APP_INITIALIZER defined? It's got the multi argument set to true after all so we can provide as many as we want to:



providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: () => initializeApp,
    multi: true,
  },
  {
    provide: APP_INITIALIZER,
    useFactory: () => doSomethingElse,
    multi: true,
  },
  {
    provide: APP_INITIALIZER,
    useFactory: () => wowAnotherThing,
    multi: true,
  },
];


Enter fullscreen mode Exit fullscreen mode

Well they all run in parallel. They'll be called in the order they were declared, but won't wait for each others to finish before executing the next one.

'Started at the same time'

What if we needed (or rather wanted) that? Even better, what if we could define different queues were within a queue, they'd execute one after another but all the queues would be started in parallel:

'Queues'

Talk is cheap. Show me the code

APP_INITIALIZER is an injection token. We want a similar capability. In order to achieve this, we'll do 2 things:

  1. Create an injection token that will be define as multi: true and that we can pass multiple times to build the different tasks of our queues
  2. Create an injection token that will be provided only once as an APP_INITIALIZER and will manage all of our queues. Once all the queues are resolved, only then we'll consider that our initilisation fase for this token is complete

Let's start with the first point.

Defining the token for all of our queues



export type AppInitializerQueueToken = string | symbol | number;

const DEFAULT_QUEUE = 'DEFAULT_QUEUE';

export interface AppInitializerQueueParam {
  queue?: AppInitializerQueueToken;
  task$: Observable<any>;
}

export const APP_INITIALIZER_QUEUE_TOKEN = new InjectionToken<AppInitializerQueueParam[]>(
  `APP_INITIALIZER_QUEUE_TOKEN`,
);


Enter fullscreen mode Exit fullscreen mode

Our APP_INITIALIZER_QUEUE_TOKEN is to be provided multiple times. Hence the array in the type.

If we look at the AppInitializerQueueParam, we see that will be able to define a queue name (could be a string but it could also be a number or a symbol to make sure a queue is really unique). It'll also be able to define a task$ as an observable.

Now let's look at the second point which is the bulk of the work where we'll manage our different queues.

Managing all the queues

We'll start by defining our provider's global shape:



export const APP_INITIALIZER_QUEUE_PROVIDER: Provider = {
  provide: APP_INITIALIZER,
  multi: true,
  useFactory: (appInitializerQueueParams: Nilable<AppInitializerQueueParam[]>) => {
    if (!appInitializerQueueParams) {
      return;
    }

    // @todo

    return () => someObservableHere$;
  },
  deps: [[new Optional(), APP_INITIALIZER_QUEUE_TOKEN]],
};


Enter fullscreen mode Exit fullscreen mode

Already quite a lot going on!

We provide as mentioned earlier, using the APP_INITIALIZER so that this initialisation function is blocking for the app.

We set multi: true because we're using APP_INITIALIZER here and that's how it works, it can be provided multiple times.

We then use a factory function to build our core logic and make sure to type it accordingly with our appInitializerQueueParams.

That params is marked as Nilable (using tsdef), because if we do not provide any APP_INITIALIZER_QUEUE_TOKEN, we'll then get null here. Which leads us to this code:



deps: [[new Optional(), APP_INITIALIZER_QUEUE_TOKEN]],


Enter fullscreen mode Exit fullscreen mode

When we provide the dependencies for our factory function, we say explicitely that providing APP_INITIALIZER_QUEUE_TOKEN is optional. The reason for this is because one could start by definining this queue provider, but not yet have built the queue tokens and we don't want the app to crash for this. Note the use of a tuple here as we cannot use the decorator @Optional as we would normally do when injecting an optional dependency from a class constructor.

Lastly, we do return () => someObservableHere$ because the APP_INITIALIZER expects a function that returns an observable (or it can a promise etc but we'll just focus on observables here).

Create one provider per queue

Remember how we'll have multiple queues, each of them having multiple tasks, and all that provided through DI? Well you have to keep in mind that our code is read line after line:

How tasks are actually provided How we want our queues to be
'How tasks are provided') 'Queues'

This is just one example of the order in which they could be declared. Note that for a given color (queue), all the tasks define the queue order by the order in which they are declared. But it doesn't mean that we have to provide them exactly one after another for the same queue. We could definitely declare tasks from other queues in between.

In order to achieve that transformation from a single array of tasks to a grouped pool of queues with ordered tasks, the first thing we need is to group our providers by queue:



const tasksByQueue = appInitializerQueueParams.reduce((acc, x) => {
  const queueName = x.queue ?? DEFAULT_QUEUE;

  if (!acc[queueName]) {
    acc[queueName] = [];
  }

  acc[queueName].push(x.task$);

  return acc;
}, {} as Record<AppInitializerQueueToken, Array<Observable<any>>>);


Enter fullscreen mode Exit fullscreen mode

Because we want to group by queue, we'll have a data structure that is a Record (a dictionary, an object) that'll take as key a queue identifier (string, symbol or number), and as a value an array of observables, representing all the tasks for the current queue.

Instantiate the providers per queue sequentially

Thanks to RxJS, instantiating all the providers of a queue sequentially is a breathe

'The feeling'



const tasksByQueue$ = Object.entries(tasksByQueue).map(([_, tasks]: [string, Array<Observable<any>>]) =>
  concat(...tasks),
);


Enter fullscreen mode Exit fullscreen mode

Note that we don't use Object.values on purpose here as it'd simply cancel all of our previous work to group tasks by queue and put all the tasks into a single array. Instead we use Object.entries to make sure we loop once per queue, and for 1 queue, we concat all the related tasks.

Start all the queues simultaneously

We now have tasksByQueue$ which is Array<Observable<any>> and we want to get instead Observable<any> and make sure that we subscribe to all in paralell:



merge(...tasksByQueue$);


Enter fullscreen mode Exit fullscreen mode

That's it!

Complete code

Usage

Here's an example of how we can then provide our tasks for different queues. Take a look at the console output:

Conclusion

Angular DI system is very powerful and so is RxJS. We've been able to implement a queue system to initialize our app with different tasks ran sequentially, but grouped by queues ran themselves in parallel. The core logic takes ~30 lines of code.

Keep in mind that any initialization attached to APP_INITIALIZER must be carefully through as this will delay the start of your app. While I'm sure there are a few cases where this could come in handy, I mostly thought of that as a little challenge with Angular and RxJS.


I hope you enjoyed this article, if you did let me know with a reaction and eventually drop a comment. It's always nice to hear back from people who took the time to read a post 😄!

If you're interested in more articles about Angular, RxJS, open source, self hosting, data privacy, feel free to hit the follow button for more. Thanks for reading!

Found a typo?

If you've found a typo, a sentence that could be improved or anything else that should be updated on this blog post, you can access it through a git repository and make a pull request. Instead of posting a comment, please go directly to https://github.com/maxime1992/my-dev.to and open a new pull request with your changes. If you're interested how I manage my dev.to posts through git and CI, read more here.

Follow me

           
Dev Github Twitter Reddit Linkedin Stackoverflow

You may also enjoy reading

. . . . . . . . . . . . . . . . . .