13:11
<nicolo-ribaudo>

Justin Ridgewell As you asked last time, here is an example where there is a significant difference between keeping the fallback stack and not doing it.

Event.captureFallbackContext(() => { // this establishes the "root context",
                                     // which is actually implicitly done by the
                                     // browser. This call is just to show it
                                     // explicitly.

  const appID = new AsyncContext.Variable();

  appID.run("Red", () => Event.captureFallbackContext(red));
  appID.run("Blue", () => Event.captureFallbackContext(blue));

  addEventListener('unhandledrejection', () => {
    console.log(`There has been an unhandled promise rejection in app ${appID.get()}`);
  });
});

where red is defined as

export function red() {
  document.addEventListener("click", () => { Promise.reject() });
}

With the current "stack" proposal

  • when the user clicks on the document, it logs There has been an unhandled promise rejection in app Red.
  • if Blue runs document.click(), it logs There has been an unhandled promise rejection in app Red.

Without the stack, only using the fallback for browser-dispatched events

  • when the user clicks on the document, it logs There has been an unhandled promise rejection in app Red.
  • if Blue runs document.click(), it logs There has been an unhandled promise rejection in app Blue.

Without the stack, using the fallback when the event not dispatched from within the same "fallback zone"

  • when the user clicks on the document, it logs There has been an unhandled promise rejection in app undefined.
  • if Blue runs document.click(), it logs There has been an unhandled promise rejection in app undefined.
13:14
<nicolo-ribaudo>

Also, I'm seeing in the notes a question about how long the bootstrap context lives.

The effects on that of

Event.captureFallbackContext(() => { addEventListener("foo", () => {}); });

are the same as

addEventListener("foo", AsyncContext.Snapshot.wrap(() => {}));
  • Event.captureFallbackContext only holds the context alive if there are event listeners registered inside it
  • differently from the other "let's always go with the registration context" approach, this only captures the context when explicitly asked to (through the .captureFallbackContext API)
13:21
<nicolo-ribaudo>

Also, I'm seeing in the notes a question about how long the bootstrap context lives.

The effects on that of

Event.captureFallbackContext(() => { addEventListener("foo", () => {}); });

are the same as

addEventListener("foo", AsyncContext.Snapshot.wrap(() => {}));
  • Event.captureFallbackContext only holds the context alive if there are event listeners registered inside it
  • differently from the other "let's always go with the registration context" approach, this only captures the context when explicitly asked to (through the .captureFallbackContext API)

Also, a big difference is that the use case of captureFallbackContext is to call it "a few times" and "close to the top-level", while event listeners are used all over the place. So the number of different snapshots captured is in general significantly smaller. Example:

Event.captureFallbackContext(() => {
  varOne.run(1, () => addEventListener("foo", () => {}));
  varTwo.run(2, () => addEventListener("foo", () => {}));
});

only captures one context, while the approach where we use the registration context by default would capture two different contexts

21:31
<Steve Hicks>

I had a question about the interaction between this and other non-event systems - suppose a framework provides lifecycle callbacks and it makes sense to have a similar treatment - do we end up nesting Event.captureFallbackContext(() => Framework.captureFallbackContext(() => ...))? Or does it make sense to generalize via (strawman)

namespace AsyncContext {
  const fallbackSnapshot = new Variable({defaultValue: new Snapshot()});
  export function captureFallback(fn) {
    return fallbackSnapshot.run(new Snapshot(), fn);
  }
  export function wrapFallback(fn) {
    const savedFallback = fallbackSnapshot.get();
    return () => savedFallback.isParentOfCurrentContext() ? fn() : savedFallback.run(fn);
  }
}

and then frameworks (or other specs) can piggyback on the same boundaries?