Skip to content
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

Standardized AsyncContext.Variable values #103

Open
dgp1130 opened this issue Aug 21, 2024 · 2 comments
Open

Standardized AsyncContext.Variable values #103

dgp1130 opened this issue Aug 21, 2024 · 2 comments

Comments

@dgp1130
Copy link

dgp1130 commented Aug 21, 2024

I'm sure this idea has come up before, but I couldn't find any existing content on it, so I figured I'd put together an issue.

I want to explore the idea of having standardized AsyncContext.Variable objects for specific use cases, much like how Symbol can create arbitrary symbols, but also contains a list of Symbol with specific semantics (ex. Symbol.iterator). Can we define specific AsyncContext variables for particular use cases which might be leveraged by standard implementations?

The main use case I'm thinking of is AbortSignal. Currently, developers much manually pass through AbortSignal into all relevant async APIs.

async function parent({ signal }: { signal?: AbortSignal } = {}): Promise<void> {
  await child({ signal });
}

async function child({ signal }: { signal?: AbortSignal } = {}): Promise<void> {
  await grandchild({ signal });
}

async function grandchild({ signal }: { signal?: AbortSignal } = {}): Promise<void> {
  const res1 = await fetch('/one', { signal });
  const res2 = await fetch('/two', { signal });
  // ...
}

To do this correctly, every async operation needs to accept a signal as an input and properly pass it through to all async functions they call. That's a lot of boilerplate and it's easy to forget.

I'd like to propose a standardized AsyncContext.signal value. In practice, this is just a standard AsyncContext.Variable containing an optional AbortSignal and defaulting to undefined.

AsyncContext.signal = new AsyncContext.Variable<AbortSignal | undefined>(undefined);

Then, anyone can read/write to this context rather than passing through signal in function parameters.

async function parent(): Promise<void> {
  await child();
}

async function child(): Promise<void> {
  await grandchild();
}

async function grandchild(): Promise<void> {
  const signal = AsyncContext.abort.get(); // Get the signal.

  // Use it.
  if (signal) {
    if (signal.aborted) return;
    signal.addEventListener('abort', () => { /* ... */ }, { once: true });
  }

  // ...
}

const signal = AbortSignal.timeout(1_000); // Create an `AbortSignal`.
await AsyncContext.abort.run(signal, () => parent()); // Automatically times out after 1sec.

Now anyone could define their own AsyncContext.Variable<AbortSignal | undefined> for this purpose, however by having a standard location for it, we get two additional benefits:

  1. AsyncContext.signal can be shared across libraries and more consistently used in the JavaScript ecosystem (different libraries don't need to define their own variable).
  2. Standard functions can use AsyncContext.signal as well.

Expanding on 2., what if fetch was aware of AsyncContext.signal and listened to it? Then, the following could work:

// No `AbortSignal` anywhere in the function definitions!
async function parent(): Promise<void> {
  await child();
}

async function child(): Promise<void> {
  await grandchild();
}

async function grandchild(): Promise<void> {
  const res1 = await fetch('/one'); // Inherits `AsyncContext.signal`.
  const res2 = await fetch('/two'); // Inherits `AsyncContext.signal`.
  // ...
}

// Run `parent()` and timeout after 1sec.
await AsyncContext.signal.run(AbortSignal.timeout(1_000), () => parent());

fetch could read AsyncContext.signal and automatically cancel the active request when it aborts!

This feels very useful to me and fixes a lot of the ecosystem problems with AbortSignal today. Not requiring developers to understand and design this concept into their APIs feels like a huge win. Aborting async operations basically "just works".

There are probably other use cases which might benefit from a standardized AsyncContext.Variable, this is just the most obvious one to me. This particular one is probably more of a follow-up standard after AsyncContext lands on its own, but I think it's worth mentioning here to foster some discussion about the use case and to use this as additional motivation for why AsyncContext could be useful.

@Jamesernator
Copy link

See also this which is basically the same thing: https://gist.github.com/littledan/47b4fe9cf9196abdcd53abee940e92df

@mmocny
Copy link

mmocny commented Sep 19, 2024

FYI @shaseley, given how prioritized task scheduling also has continuation preserved signal.

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

3 participants