async_hooks: add SymbolDispose support to AsyncLocalStorage
This introduces explicit resource management support to AsyncLocalStorage. In order to avoid unexpected leakage of an async local storage value to outer scope, asyncLocalStorage.run
requires a callback to nest all sub-procedures in a new function scope. However, when using AsyncLocalStorage
with different function types, like async generator, moving statements and expression to a new function body could be evaluated in different ways.
For example, given an async generator:
class C {
async* foo() {
await a();
yield b();
await c();
}
}
Then, if we want to modify the async local storage value for b()
and c()
, simply moving statements would not work:
const storage = new AsyncLocalStorage();
class C {
async* foo() {
await a();
storage.run(value, () => {
yield b(); // syntax error, arrow function is not a generator
await this.c(); // syntax error, arrow function is not async
});
}
}
Making the arrow function to be an async generator as well still requires significant refactoring:
const storage = new AsyncLocalStorage();
class C {
async* foo() {
await a();
yield* storage.run(value, async function*() => {
yield b(); // ok
await this.c(); // reference error, this is undefined
});
}
}
This could be potentially more convenient with using
declarations:
const storage = new AsyncLocalStorage();
class C {
async* foo() {
await a();
using _ = storage.disposableStore(value);
yield b();
await this.c();
}
}
However, creating a disposable without a using
declaration could still be a problem leading to leakage of the async local storage value. To help identifying such mis-use, an ERR_ASYNC_LOCAL_STORAGE_NOT_DISPOSED
error is thrown if an AsyncLocalStorageDisposableStore
instance is not disposed at the end of the current async resource scope.