Bring Thread-Local-Storage / Async_Hooks into the language

My company has adopted the use of thread-local-storage (via Zone.js) to allow the same code to run in two different contexts; The first, and default context is one in which the code actively navigates the user to another location in the browser. The second context is where we capture the final URL instead, so that it can be inserted into the href of an anchor tag.

At bare minimum, I need to know "what triggered the execution of this callback?". Having access to "Thread local storage" (Zone.current in my case combined with a WeakMap) has allowed the code to be less error prone, as we're not forced to pass a context object around with every function invocation in the stack. It has allowed our application to comply with WCAG guidelines more completely because what was previously a click handler function, can now be expressed as a full HREF. It always works, because it's the exact same codepath, save for whether or not navigation occurs or it's just captured.

Node.JS has the async_hooks feature, and for now, I have Zone.js. However, it would be a lot easier knowing the rug isn't going to be pulled out from underneath me; Zone.js is being deprecated because of its inability to handle async / await, and without a replacement I'll be unable to use this very powerful technique.

While we can still operate without native support for this feature, the confidence we have that link generation will be correct is something I don't want to lose. Alternatives would require a rewrite with a context object passed through at every step, and I feel like Javascript can be better than that.

For reference, here is the critical code I will be unable to use if I lose access to Zone.js and don't have an alternative.

@Injectable({
  providedIn: 'root',
})
export class FlowNavRecorderService {
  get useRecorder(): boolean {
    return !!this._useRecorder.get(Zone.current);
  }

  private _capturedRoute = new WeakMap<
    Zone,
    BehaviorSubject<IRoute | { href: string } | null>
  >();
  private _useRecorder = new WeakMap<Zone, boolean>();

  getPlannedRoute() {
    return this._capturedRoute.get(Zone.current);
  }
  resetPlannedRoute() {
    this._capturedRoute.get(Zone.current)?.next(null);
  }

  constructor() {}

  recordPlannedAction(plannedRoute: IRoute | { href: string }) {
    this._capturedRoute.get(Zone.current).next(plannedRoute);
  }

  private _it = 0;

  capturePlannedRoute(
    doNavigation: () => void
  ): Promise<IRoute | { href: string }> {
    const zone = Zone.current.fork({
      name: 'flow-nav-recorder-capture--' + this._it++,
    });
    const { _capturedRoute, _useRecorder } = this;
    return new Promise<IRoute | { href: string }>((resolve, reject) => {
      const route$ = new BehaviorSubject<IRoute | { href: string } | null>(
        null
      );
      _capturedRoute.set(zone, route$);
      _useRecorder.set(zone, true);
      zone.run(() => {
        try {
          doNavigation();
        } catch {}
        route$.subscribe(
          (r) => {
            if (!r) {
              return;
            }
            resolve(r);
            route$.complete();
            _useRecorder.delete(zone);
            _capturedRoute.delete(zone);
          },
          (err) => {
            reject(err);
          }
        );
      });
    });
  }

}

If I had access to async_hooks, I'm confident I could do a rewrite without too many downstream effects. What can we do here? Can it be rolled into the async do proposal?

GitHub - legendecas/proposal-async-context was on the agenda for TC39 last month, but unfortunately there wasn't enough time so it's been deferred to a future meeting.

Slides: AsyncContext - Google Slides

So the needle has to be threaded very carefully here. In order to avoid full on dynamic scoping, unconditionally exposing information allowing a function to sense the caller is not going to be acceptable. In that sense, a global like Zone.current is not a solution.

The AsyncContext proposal takes pain to avoid the dynamic scoping issues by requiring the function to hold a reference to the AsyncContext instance. It requires a predecessor (synchronous or asynchronous) to initialize the context's content, and for the program to arrange the same context instance being shared between the predecessor and the function that requires the context's data. That allows a set of related functions to share "stack" state independently of the interleaving functions on the stack.

Of course at that point nothing prevents the functions using such a context from having a behavior observably different outside of the functions sharing a reference to the context, but good practices should avoid that as it would recreate the problems caused by dynamic scoping.

That gives me hope.

Is the prospect good at getting it in browsers within the next 1, 2 years?

I can't guarantee how timing will work out for this proposal, or if it will make it at all, it really depends.

One thing though is that this proposal has a new co-champion, @jridgewell, who is very interested in getting it through, so I'd be hopeful something will come out of it that should solve your use case, even if it doesn't look exactly like the current proposal.

Yes, zones isn't really the necessary part, async context would be enough. How can we help @jridgewell make a case for it?