diff --git a/.eslintrc.js b/.eslintrc.js index a154d00794ab4d..84c7b8e439c694 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,7 @@ const hacks = [ 'eslint-plugin-markdown', '@babel/eslint-parser', '@babel/plugin-syntax-import-attributes', + '@babel/plugin-proposal-explicit-resource-management', ]; Module._findPath = (request, paths, isMain) => { const r = ModuleFindPath(request, paths, isMain); @@ -45,6 +46,7 @@ module.exports = { babelOptions: { plugins: [ Module._findPath('@babel/plugin-syntax-import-attributes'), + Module._findPath('@babel/plugin-proposal-explicit-resource-management'), ], }, requireConfigFile: false, diff --git a/doc/api/async_context.md b/doc/api/async_context.md index d1be2fb3807e17..91207d112d702d 100644 --- a/doc/api/async_context.md +++ b/doc/api/async_context.md @@ -361,6 +361,57 @@ try { } ``` +### `asyncLocalStorage.disposableStore(store)` + + + +> Stability: 1 - Experimental + +* `store` {any} +* Returns: {AsyncLocalStorageDisposableStore} + +Transitions into the context until the returned +[`AsyncLocalStorageDisposableStore`][] instance is disposed, and then persists +the store through any following asynchronous calls. + +Example: + +```js +const store = { id: 1 }; +{ + // Replaces previous store with the given store object + using _ = asyncLocalStorage.disposableStore(store); + asyncLocalStorage.getStore(); // Returns the store object + someAsyncOperation(() => { + asyncLocalStorage.getStore(); // Returns the same object + }); + // The store goes out of scope +} +asyncLocalStorage.getStore(); // Returns undefined +``` + +This transition will continue for the _entire_ synchronous execution scope +until the returned `AsyncLocalStorageDisposableStore` instance is disposed. + +```js +const store = { id: 1 }; + +emitter.on('my-event', () => { + // Declare a disposable store with using statement. + using _ = asyncLocalStorage.disposableStore(store); +}); +emitter.on('my-event', () => { + asyncLocalStorage.getStore(); // Returns undefined +}); + +asyncLocalStorage.getStore(); // Returns undefined +emitter.emit('my-event'); +asyncLocalStorage.getStore(); // Returns undefined +``` + ### Usage with `async/await` If, within an async function, only one `await` call is to run within a context, @@ -395,6 +446,83 @@ of `asyncLocalStorage.getStore()` after the calls you suspect are responsible for the loss. When the code logs `undefined`, the last callback called is probably responsible for the context loss. +## Class: `AsyncLocalStorageDisposableStore` + + + +> Stability: 1 - Experimental + +A utility class that enables `using` statement to dispose of a store value for +an `AsyncLocalStorage` store when the current scope was exited. + +### `asyncLocalStorageDisposableStore[Symbol.dispose]()` + +Restores the previous store for the creation `AsyncLocalStorage`. + +### Usage with `AsyncLocalStorage` + +```js +const http = require('node:http'); +const { AsyncLocalStorage } = require('node:async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +function logWithId(msg) { + const id = asyncLocalStorage.getStore(); + console.log(`${id !== undefined ? id : '-'}:`, msg); +} + +let idSeq = 0; +http.createServer((req, res) => { + using _ = asyncLocalStorage.disposableStore(idSeq++); + logWithId('start'); + // Imagine any chain of async operations here + setImmediate(() => { + logWithId('finish'); + res.end(); + }); +}).listen(8080); + +http.get('http://localhost:8080'); +http.get('http://localhost:8080'); +// Prints: +// 0: start +// 1: start +// 0: finish +// 1: finish +``` + +### Troubleshooting: Instance not disposed + +The instantiation of the [`AsyncLocalStorageDisposableStore`][] can be used +without the using statement. This can be problematic that if, for example, the +context is entered within an event handler and the +`AsyncLocalStorageDisposableStore` instance is not disposed, subsequent +event handlers will also run within that context unless specifically bound to +another context with an `AsyncResource`. + +```js +const store = { id: 1 }; + +emitter.on('leaking-event', () => { + // Declare a disposable store with variable declaration and did not dispose it. + const _ = asyncLocalStorage.disposableStore(store); +}); +emitter.on('leaking-event', () => { + asyncLocalStorage.getStore(); // Returns the store object +}); + +asyncLocalStorage.getStore(); // Returns undefined +emitter.emit('my-event'); +asyncLocalStorage.getStore(); // Returns the store object +``` + +To hint such a misuse, 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. + ## Class: `AsyncResource`