Skip to content

APP_INITIALIZER won't work with delayed promises #1487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Peluko opened this issue Aug 15, 2018 · 13 comments
Open

APP_INITIALIZER won't work with delayed promises #1487

Peluko opened this issue Aug 15, 2018 · 13 comments

Comments

@Peluko
Copy link

Peluko commented Aug 15, 2018

Make sure to check the existing issues in this repository

Checked.

If there is no issue for your problem, tell us about it

I'm trying Angular's APP_INITIALIZER to initialize app configuration before app launches (SQLite reading). Angular's documentation states that it will wait until the promises on APP_INITIALIZER are resolved to start the application. But on Nativescript you always obtain the error 'Bootstrap promise didn't resolve' whenever you use a delayed Promise on APP_INITIALIZER. This simple code produces the error:

function testInitializer(): Promise<void> {
    return new Promise((resolve: () => void) =>
        setTimeout(() => resolve(), 10)
    );
}

@NgModule({
    bootstrap: [
        AppComponent
    ],
    imports: [
        NativeScriptModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent
    ],
    providers: [
        { provide: APP_INITIALIZER, useFactory: () => testInitializer, multi: true }
    ],
    schemas: [
        NO_ERRORS_SCHEMA
    ]
})
export class AppModule { }

However, if you return a resolved promise, it works. For example, if you change the initialization funtion to:

function testInitializer(): Promise<void> {
    return Promise.resolve();
}

it boots correctly.

I think that the problem is with the following code on platform-common.js, which doesn't wait for promises before checking the value of bootstrapPromiseCompleted:

let bootstrapPromiseCompleted = false;
this._bootstrapper().then(
moduleRef => {
bootstrapPromiseCompleted = true;
bootstrapLog(`Angular bootstrap bootstrap done. uptime: ${uptime()}`);
if (!autoCreateFrame) {
rootContent = tempAppHostView.content;
}
lastBootstrappedModule = new WeakRef(moduleRef);
},
err => {
bootstrapPromiseCompleted = true;
const errorMessage = err.message + "\n\n" + err.stack;
bootstrapLogError("ERROR BOOTSTRAPPING ANGULAR");
bootstrapLogError(errorMessage);
rootContent = this.createErrorUI(errorMessage);
}
);
bootstrapLog("bootstrapAction called, draining micro tasks queue. Root: " + rootContent);
(<any>global).Zone.drainMicroTaskQueue();
bootstrapLog("bootstrapAction called, draining micro tasks queue finished! Root: " + rootContent);
if (!bootstrapPromiseCompleted) {
const errorMessage = "Bootstrap promise didn't resolve";
bootstrapLogError(errorMessage);
rootContent = this.createErrorUI(errorMessage);
}

Which platform(s) does your issue occur on?

  • iOS and Android, both emulator and device

Please, provide the following version numbers that your issue occurs with:

It can be easily reproduced on Playground: APP_INITIALIZER_PROMISES

Please, tell us how to recreate the issue in as much detail as possible.

The above mentioned Playground does it well.

Is there any code involved?

@vakrilov
Copy link
Contributor

Hey @Peluko - you are indeed correct.
Тhe angular bootstrap is done in the launchEvent. The NativeScript framework expects that this event handler will return the root view for the app (inside args.root). Usually, the angular bootstrap resolves synchronously and so we are able to get the resulting root view and pass it to NativeScript (trough args.root). Having async app initializer causes the bootstrap promise to resolve async and so there is no view that we can give to the NativeScript framework to render.

Currently, there is no workaround for using async app initializer. Maybe you can do the async job inside your AppComponent or in an angular route resolver service.

There are a couple approaches we have discussed for overcoming this:

  1. Extending logic handling the application launchEvent inside NativeScript, so that you can return a promise.
  2. Changing the angular bootstrap so that if the promise is not resolved immediately - we return a temporary empty view and than when the promise is resolved - we replace the application root with the view returned form the async bootstrap.

@kspearrin
Copy link

kspearrin commented Oct 2, 2018

I am also running into this issue.

@vakrilov Regarding point 2, couldn't the splash screen continue to show until it is resolved?

@vedranstanic82
Copy link

Maybe you can do the async job inside your AppComponent or in an angular route resolver service.

Hi @vakrilov - we are trying to grab a 100kb json file from a server as the app is initializing/loading. This json will be used in the initialisation of the NS TabView template pages as the source of data for all tabs. The TabView has the page-router-outlet mechanism as we started with the NT Tab template:

<page-router-outlet *tabItem="{title: 'Scheduler', iconSource: getIconSource('search')}" name="schedulerTab"> </page-router-outlet>

How can we make sure that we first have the json data from the server grabbed, so that we can proceed with the app initialisation, or in other words with using that data to draw out all of the tab screens/pages? Currently the TabView inits all tabs immediately without waiting for the getJSON to complete and we get JS errors (undefined) in the tab ts files. Do you have a recommended approach, and if so if you could provide some details or a link on how to do it that would be super appreciated.

Thanks!

Vedran

@vakrilov
Copy link
Contributor

Have you looked into the resolver service? Maybe you can add one to your default path (you should have a page-router-outlet outlet as the root of your app).
Other approach might be to have a loader component in your default path and trigger the loading inside it. Once the data is loaded you can navigate with clearHistory to your tabbed component.

@vedranstanic82
Copy link

Thanks @vakrilov, we went with your suggested approach (resolver) and we added it to the first child page route of the tabview component (1st tab page route).

The tabview component is a little bit special it seems in how it connects the routes with the tabs and the whole initialisation of it, although we are new to NS so it could be just us. We tried the "loading component" approach and then navigating to the tabbed component once resolver was done, but having a tabview with child page-router-outlets seems to collide with the app root child-page-router. Perhaps we need to have nested routers in that case? I'm sure this is confusing to read and a Playground example would be much better, but the 1st tab resolver approach seems to work so far with the resolver attached to the tabview child route (1st tab route), although it is yet to be seen how nice of a transition we will be able to make as the app loads for a longer period of time and then renders the 1st tab.

That's why the loading component approach sounds attractive, but not sure how to get that transition from the loading component to the tabview working. The tabview breaks in terms of rendering itself...does the tab view need to sit in its own module, or do we need to have two page-router-outlets, one for the original app and one for the tabs inside the tabview? thnx

@DimitarTachev
Copy link

A similar error is reproducible during live sync when debugging navigations (Livesync bootstrap promise didn't resolve).
https://github.com/NativeScript/nativescript-angular/blob/master/nativescript-angular/platform-common.ts#L328

Steps to reproduce:

  1. npm i [email protected] -g && tns create appNG –ng && cd appNG
  2. tns debug ios
  3. Set breakpoint in ngOnInit of item-detail.component.ts and tap on an item to hit it.
  4. Edit a css file in order to trigger the livesync process.
  5. You will get the Livesync bootstrap promise didn't resolve error in the Simulator.

As far as I see, the error is caused by the same (<any>global).Zone.drainMicroTaskQueue(); call. We could think about an alternative solution like locking a local object instead of relying on the zone.

@csimpi
Copy link

csimpi commented Feb 11, 2020

Pls, fix this, there's no way to initialize things before the APP would start which is a huge issue when we're talking real Apps

@flodaniel
Copy link

we are also coming across this issue now. we expected it would be easy to implement, because in angular it is. but sadly impossible in nativescript to have a nice implementation. :/

@csimpi
Copy link

csimpi commented Mar 19, 2020

@Firetrip Currently the only way to solve this is the route resolvers. I'm not comfortable with it but there's no other chance.

@flodaniel
Copy link

@csimpi but how do you handle the wait time? The splashscreen already disappears at this point :(

@edusperoni
Copy link
Collaborator

You could create a "splash" view which displays the app logo and acts like an extended splash screen, so your first route would be splash screen which immediately goes to the next route with a resolver. AFAIK there's no way to extend the native splash screen. Even in the native space the recommended way is to create a new activity with the splash (https://devdeeds.com/how-to-create-a-5-seconds-splash-screen-in-android/)

As soon as your app is loaded android will hide the splashscreen. Best thing you can do is show an artificial one to circumvent this native limitation.

@flodaniel
Copy link

I tried that, but you can see the animation.
This is my route structure and instead of a resolver i do the async logic and the navigation in the SplashComponent.

const routes: Routes = [
    {
        path: "",
        component: SplashComponent,
    },
    {
        path: "pre-login",
        loadChildren: () => import("~/app/pre-login/pre-login.module").then((m) => m.PreLoginModule)
    },
    {
        path: "post-login",
        loadChildren: () => import("~/app/post-login/post-login.module").then((m) => m.PostLoginModule),
        canActivate: [LoginActivate]
    },
];

@hkthiet2999
Copy link

hkthiet2999 commented Mar 25, 2025

Please help to fix, i'm also falling on this issue with this

export function applicationInit(
  translateService: CTranslateService | CTranslateServiceV2,
): () => Promise<boolean> {
  return async (): Promise<boolean> => {

    await Promise.all([waitForTranslationInit(translateService)]);

    return true;
  };
}


async function waitForTranslationInit(
  translateService: CTranslateService | CTranslateServiceV2,
): Promise<void> {
 //...
await translateService.onInitialized();
    return Promise.resolve();
}

/** translateService
*/
  public onInitialized(): Promise<void> {
    return new Promise((resolve) => {
      if (this.isInitialized) {
        resolve();
      } else {
        this.initializedSubject.subscribe((initialized) => {
          if (initialized) {
            resolve();
          }
        });
      }
    });
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants