3

I'm creating an application where the user can identify himself via his Google account. Behind the scenes, I'm using gapi to handle the login. On the other hand, there is an angular service called "user" that has an Observable that is broadcasting information to subscribers every time the status (online/offline) of the user changes. Then, to glue everything together, there is a button and when the user clicks on it, here's what happen:

  • A promise is created that resolves once gapi is initialized. (1st line of the code below)
  • When the promise resolves, the Google login is performed via gapi. (2nd line of the code below)
  • When the login promise resolves, an AJAX request id made to the backend to verify the Google token and returns information such as the email, the name, etc. and subscribe to this observable. (the line starting with this.restService in the code below)
  • When the observable above emits the value returned by my web service, I call a function in my "user" service that emits a value on the observable for the status of the user (it broadcast the fact that the user is now authenticated).
  • Then, all subscribers receive this information and know that the user is logged in.

Here is the code:

this.ensureApiIsLoaded().then(() => {
    this.auth.signIn().then((user) => {
        let profile = user.getBasicProfile();
        this.restService
            .post("/api/security/authenticate", <IAuthenticationPayload>{ type: AuthenticationType.Google, token: user.getAuthResponse().id_token })
            .subscribe((data: IUserData) => {
                this.userService.set("data.name", "data.email", "data.picture", AuthenticationType.Google);
            });
    });
});

The thing is that code sometimes works and sometimes not. After some investigation, I noticed that it was related to the duration of the my web service execution. To ensure this, I made a statement inside it that pauses the execution of the request during 2 seconds et in that case, my code always fails. But what do I mean by "fails"?

Somewhere on a page, I have a component that is subscribed to the user service observable:

this.userService.getStatus().subscribe(status => {
    console.log(status);
    this.canPostComment = (status == UserStatus.Online);
});

When the web service executes very fast, I see the console.log, then the canPostComment property is updated and so is my view, so no issue. However, when the web service takes a bit longer, I still see the console.log with the correct value but the view is not updated...

Suspecting that it has something to do with Angular changes detection, I use zone this way:

this.ensureApiIsLoaded().then(() => {
    this.auth.signIn().then((user) => {
        let profile = user.getBasicProfile();
        this.zoneService.run(() => {
            this.restService
                .post("/api/security/authenticate", <IAuthenticationPayload>{ type: AuthenticationType.Google, token: user.getAuthResponse().id_token })
                .subscribe((data: IUserData) => {
                    this.userService.set("data.name", "data.email", "data.picture", AuthenticationType.Google);
                });
        });
    });
});

And then it works... So I read about zone and I learnt that it was used to warn angular that it has to run changes detection (or something like that...), but I also read that common functions like setTimeout or AJAX call are already monkey-patched by "zone", so I don't get how it solves my issue and I don't really get why I have this issue. Why doesn't Angular see that canPostComment has changed?

As my code is a bit complex, it would be quite hard to plunkr'd it, that's why I copy/paste most of the relevant code.

EDIT

After I asked the question, I had to modify a bit my code because I needed to know when the whole login process was done. Indeed, when the user clicks on the login button, its caption becomes "Logging in..." and once the whole process is completed, it disappears. I could have subscribed to the userService.getStatus observable, but I decided to go with a promise to be able to do:

this.googleAuthenticationService.login.then(() => { ... });

So I updated the code above this way:

return new Promise((resolve, reject) => {
    this.ensureApiIsLoaded().then(() => {
        this.auth.signIn().then((user) => {
            let profile = user.getBasicProfile();
            this.zoneService.run(() => {
                this.restService
                    .post("/api/security/authenticate", <IAuthenticationPayload>{ type: AuthenticationType.Google, token: user.getAuthResponse().id_token })
                    .subscribe((data: IUserData) => {
                        this.userService.set(data.name, data.email, data.picture, AuthenticationType.Google);

                        resolve(true)
                    },
                    error => reject(false));
            });
        });
    });
});

Out of curiosity, I tried to remove the this.zoneService.run statement just to see what happens and it turns out that it works without... So putting everything in a promise seems to fix the issue... However, the question still remains... Why didn't the first example work?

ssougnez
  • 5,315
  • 11
  • 46
  • 79
  • I don't have a full answer, but this blog post gives a pretty good explanation on Zones: https://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html. Angular hooks into the Zone.js to run a change detection cycle after every VM turn (that is, after any single task -- and all subsequent microtasks -- has completed). This Zone is controlled by Angular. If you make changes during the change detection cycle (*i.e., `ApplicationRef.tick()` is in the stack*), then you haven't begun a new task and change detection won't re-run. You could check for this to get an idea of what's going on. – Mike Hill Jul 05 '17 at 21:35
  • Hehe, that's the article I was refering to in the OP. But I'll read it once again, maybe I missed something. – ssougnez Jul 05 '17 at 21:37
  • Also, there's a small chance that it's not running the child promise within Angular's zone. I don't know why it wouldn't, but just in case you may consider chaining your promises rather than creating sub-promises: `this.ensureApiIsLoaded().then(() => this.auth.signIn()).then((user) => {...});` – Mike Hill Jul 05 '17 at 21:37
  • can you add the following after the `console.log(status);` statement `console.log('Zone is: ', Zone.current.name);`? – Max Koretskyi Jul 06 '17 at 04:46
  • Hi, where does the "Zone" object comes from? I tried to inject an NgZone one, but there is no "current" property in it. However, By trying to do so, I noticed that I don't need Zone anymore if I wrap everything in a promise. Indeed, I updated the code to be aware of when the whole process is done, so I added `new Promise...` around it and now, when I remove the `zone.run` part, it works, whatever the time took by my web service... I'll update the OP. – ssougnez Jul 06 '17 at 06:25
  • `Zone` is a global. If you're using Angular CLI then it gets injected via the line `import 'zone.js/dist/zone';` in `polyfills.ts`. By the way, your change with wrapping the code in a promise seems to indicate that the inner promises aren't being created with the same zone. I wonder if the inner promises/subscriptions are what's causing the issue with Angular's CD altogether. Here's a gist that chains all of the operations together in a single stream: https://gist.github.com/hill0826/ab624284066a9fbc47115952a2a1c877. Can you try that out and see if the change detection issue is still there? – Mike Hill Jul 06 '17 at 14:06
  • Also, if it's running in the `angular` zone then I think the other likely issue is that it's running during a change detection cycle. If you inject `ApplicationRef` then you can check to see if a CD cycle is currently being run via `console.log((this.applicationRef as any)['_runningTick']);`. If that prints `true` then a CD cycle is active and changes are not guaranteed to be detected until the next CD check is triggered for the component. – Mike Hill Jul 06 '17 at 14:35

0 Answers0