3

First and foremost, I'm well aware of zone.runOutsideAngular(callback), documented here .

What this method does is running the callback in a different zone than the Angular one.

I need to attach a very quick callback to document's mousemove event (to count Idle time). I don't want to have the whole Zone.JS machinery of tasks appended to execution queues for this specific callback. I would really like to have the callbacks run in plain, unpatched browser runtime.

The event registration code I have is:

  private registerIdleCallback(callback: (idleFromSeconds: number) => void) {
    let idleFromSeconds = 0;
    const resetCounter = function() {
      idleFromSeconds = -1;
    };
    document.addEventListener('mousemove', resetCounter);
    document.addEventListener('keypress', resetCounter);
    document.addEventListener('touchstart', resetCounter);
    window.setInterval(function() {
      callback(++idleFromSeconds);
    }, 1000);
  }

The question is how can I get this code to use the unpatched document.addEventListener, accomplishing a complete separation from Zone.JS and true native performance?

2 Answers 2

3

Well, it turned out to be simple, but not straightforward, so I'm going to post the solution I found here.

What I want to do is saving a usable document.addEventListener in a global object before Zone.JS patches the hell out of the browser's native objects.

This must be done before the loading of polyfills, because it's in there that Zone is loaded. And, it must not be done in plain code in polyfills.ts, because the import statements are processed before any other code.

So I need a file zone-config.ts in the same folder of polyfills.ts. In the latter an extra import is needed:

import './zone-config'; // <- adding this line to the file, before...
// Zone JS is required by default for Angular itself.
import 'zone.js/dist/zone'; // Included with Angular CLI.

In zone-config.ts I do the sleight-of-hand:

(function(global) {
  // Save native handlers in the window.unpatched object.
  if (global.unpatched) return;

  if (global.Zone) {
    throw Error('Zone already running: cannot access native listeners');
  }

  global.unpatched = {
    windowAddEventListener: window.addEventListener.bind(window),
    windowRemoveEventListener: window.removeEventListener.bind(window),
    documentAddEventListener: document.addEventListener.bind(document),
    documentRemoveEventListener: document.removeEventListener.bind(document)
  };

  // Disable Zone.JS patching of WebSocket -- UNRELATED TO THIS QUESTION
  const propsArray = global.__Zone_ignore_on_properties || (global.__Zone_ignore_on_properties = []);
  propsArray.push({ target: WebSocket.prototype, ignoreProperties: ['close', 'error', 'open', 'message'] });

  // disable addEventListener
  const evtsArray = global.__zone_symbol__BLACK_LISTED_EVENTS || (global.__zone_symbol__BLACK_LISTED_EVENTS = []);
  evtsArray.push('message');
})(<any>window);

Now I have a window.unpatched object available, that allows me to opt out of Zone.JS completely, for very specific tasks, like the IdleService I was working on:

import { Injectable, NgZone } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';

/* This is the object initialized in zone-config.ts */
const unpatched: {
  windowAddEventListener: typeof window.addEventListener;
  windowRemoveEventListener: typeof window.removeEventListener;
  documentAddEventListener: typeof document.addEventListener;
  documentRemoveEventListener: typeof document.removeEventListener;
} = window['unpatched'];

/** Manages event handlers used to detect User activity on the page,
 * either via mouse or keyboard events. */
@Injectable({
  providedIn: 'root'
})
export class IdleService {
  private _idleForSecond$ = new BehaviorSubject<number>(0);

  constructor(zone: NgZone) {
    const timerCallback = (idleFromSeconds: number): void => this._idleForSecond$.next(idleFromSeconds);
    this.registerIdleCallback(timerCallback);
  }

  private registerIdleCallback(callback: (idleFromSeconds: number) => void) {
    let idleFromSeconds = 0;
    const resetCounter = () => (idleFromSeconds = -1);

    // runs entirely out of Zone.JS
    unpatched.documentAddEventListener('mousemove', resetCounter);
    unpatched.documentAddEventListener('keypress', resetCounter);
    unpatched.documentAddEventListener('touchstart', resetCounter);

    // runs in the Zone normally
    window.setInterval(() => callback(++idleFromSeconds), 1000);
  }

  /** Returns an observable that emits when the user's inactivity time
   * surpasses a certain threshold.
   *
   * @param seconds The minimum inactivity time in seconds.
   */
  byAtLeast(seconds: number): Observable<number> {
    return this._idleForSecond$.pipe(filter(idleTime => idleTime >= seconds));
  }

  /** Returns an observable that emits every second the number of seconds since the
   * ladt user's activity.
   */
  by(): Observable<number> {
    return this._idleForSecond$.asObservable();
  }
}

Now the stackTrace in the (idleFromSeconds = -1) callback is empty as desired.

Hope this can be of help to someone else: it's not complicated, but having all the bits in place was a bit of a chore.

Sign up to request clarification or add additional context in comments.

2 Comments

Is there a way to use this to make sure calls from 3rd party libraries are unpatched. I have calls to web sockets and web workers coming from 3rd parties that are getting patched.
Not using exactly the same code, but you can use the part using the __zone_symbol__BLACK_LISTED_EVENTS global object, and fill it taking inspiration from my example and from the (somewhat sparse) documentation at github.com/angular/angular/blob/master/packages/zone.js/…. If you fill the __zone_symbol__BLACK_LISTED_EVENTS object before zone.js patches the page, everyone calling into WebSockets or other API will use the native version. Just remember that, outside of Zone, you are in charge of getting back into Angular-land (via NgZone or ChangeDetectorRef).
1

I am not sure if I got what you want to do. I have made a little plunker that shows how to do an event callback from outside of Angular.

https://stackblitz.com/edit/angular-s3krnu

4 Comments

Thanks! I can't seem to find where do you declare the registerEventCallback function. How is it referenced? Maybe you could add to the answer how it is implemented.
Have a look at the index.html also.
No, that will not work, because when the registerEventCallback will be invoked, it will run against a patched document object. I'm currently trying to save the unpatched functions into a separate object, for later use.
I perfected the approach and came up with a working solution. Thanks for the input anyway!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.