-
-
Notifications
You must be signed in to change notification settings - Fork 34.6k
async_hooks: add using scopes to AsyncLocalStorage #61674
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -386,6 +386,83 @@ try { | |||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ### `asyncLocalStorage.withScope(store)` | ||||||
|
|
||||||
| <!-- YAML | ||||||
| added: REPLACEME | ||||||
| --> | ||||||
|
|
||||||
| > Stability: 1 - Experimental | ||||||
|
|
||||||
| * `store` {any} | ||||||
| * Returns: {RunScope} | ||||||
|
|
||||||
| Creates a disposable scope that enters the given store and automatically | ||||||
| restores the previous store value when the scope is disposed. This method is | ||||||
| designed to work with JavaScript's explicit resource management (`using` syntax). | ||||||
|
|
||||||
| Example: | ||||||
|
|
||||||
| ```mjs | ||||||
| import { AsyncLocalStorage } from 'node:async_hooks'; | ||||||
|
|
||||||
| const asyncLocalStorage = new AsyncLocalStorage(); | ||||||
|
|
||||||
| { | ||||||
| using scope = asyncLocalStorage.withScope('my-store'); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think this is the usual guideline if unused. Feel free to ignore. |
||||||
| console.log(asyncLocalStorage.getStore()); // Prints: my-store | ||||||
| } | ||||||
|
|
||||||
| console.log(asyncLocalStorage.getStore()); // Prints: undefined | ||||||
| ``` | ||||||
|
|
||||||
| ```cjs | ||||||
| const { AsyncLocalStorage } = require('node:async_hooks'); | ||||||
|
|
||||||
| const asyncLocalStorage = new AsyncLocalStorage(); | ||||||
|
|
||||||
| { | ||||||
| using scope = asyncLocalStorage.withScope('my-store'); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| console.log(asyncLocalStorage.getStore()); // Prints: my-store | ||||||
| } | ||||||
|
|
||||||
| console.log(asyncLocalStorage.getStore()); // Prints: undefined | ||||||
| ``` | ||||||
|
|
||||||
| The `withScope()` method is particularly useful for managing context in | ||||||
| synchronous code where you want to ensure the previous store value is restored | ||||||
| when exiting a block, even if an error is thrown. | ||||||
|
|
||||||
| ```mjs | ||||||
| import { AsyncLocalStorage } from 'node:async_hooks'; | ||||||
|
|
||||||
| const asyncLocalStorage = new AsyncLocalStorage(); | ||||||
|
|
||||||
| try { | ||||||
| using scope = asyncLocalStorage.withScope('my-store'); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| console.log(asyncLocalStorage.getStore()); // Prints: my-store | ||||||
| throw new Error('test'); | ||||||
| } catch (e) { | ||||||
| // Store is automatically restored even after error | ||||||
| console.log(asyncLocalStorage.getStore()); // Prints: undefined | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ```cjs | ||||||
| const { AsyncLocalStorage } = require('node:async_hooks'); | ||||||
|
|
||||||
| const asyncLocalStorage = new AsyncLocalStorage(); | ||||||
|
|
||||||
| try { | ||||||
| using scope = asyncLocalStorage.withScope('my-store'); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| console.log(asyncLocalStorage.getStore()); // Prints: my-store | ||||||
| throw new Error('test'); | ||||||
| } catch (e) { | ||||||
| // Store is automatically restored even after error | ||||||
| console.log(asyncLocalStorage.getStore()); // Prints: undefined | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ### Usage with `async/await` | ||||||
|
|
||||||
| If, within an async function, only one `await` call is to run within a context, | ||||||
|
|
@@ -420,6 +497,22 @@ 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: `RunScope` | ||||||
|
|
||||||
| <!-- YAML | ||||||
| added: REPLACEME | ||||||
| --> | ||||||
|
|
||||||
| > Stability: 1 - Experimental | ||||||
|
|
||||||
| A disposable scope returned by [`asyncLocalStorage.withScope()`][] that | ||||||
| automatically restores the previous store value when disposed. This class | ||||||
| implements the [Explicit Resource Management][] protocol and is designed to work | ||||||
| with JavaScript's `using` syntax. | ||||||
|
|
||||||
| The scope automatically restores the previous store value when the `using` block | ||||||
| exits, whether through normal completion or by throwing an error. | ||||||
|
|
||||||
| ## Class: `AsyncResource` | ||||||
|
|
||||||
| <!-- YAML | ||||||
|
|
@@ -905,8 +998,10 @@ const server = createServer((req, res) => { | |||||
| }).listen(3000); | ||||||
| ``` | ||||||
|
|
||||||
| [Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management | ||||||
| [`AsyncResource`]: #class-asyncresource | ||||||
| [`EventEmitter`]: events.md#class-eventemitter | ||||||
| [`Stream`]: stream.md#stream | ||||||
| [`Worker`]: worker_threads.md#class-worker | ||||||
| [`asyncLocalStorage.withScope()`]: #asynclocalstoragewithscopestore | ||||||
| [`util.promisify()`]: util.md#utilpromisifyoriginal | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| 'use strict'; | ||
|
|
||
| const { | ||
| SymbolDispose, | ||
| } = primordials; | ||
|
|
||
| class RunScope { | ||
| #storage; | ||
| #previousStore; | ||
| #disposed = false; | ||
|
|
||
| constructor(storage, store) { | ||
| this.#storage = storage; | ||
| this.#previousStore = storage.getStore(); | ||
| storage.enterWith(store); | ||
| } | ||
|
|
||
| [SymbolDispose]() { | ||
| if (this.#disposed) { | ||
| return; | ||
| } | ||
| this.#disposed = true; | ||
| this.#storage.enterWith(this.#previousStore); | ||
| } | ||
| } | ||
|
|
||
| module.exports = RunScope; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| /* eslint-disable no-unused-vars */ | ||
| 'use strict'; | ||
| require('../common'); | ||
| const assert = require('node:assert'); | ||
| const { AsyncLocalStorage } = require('node:async_hooks'); | ||
|
|
||
| // Test basic RunScope with using | ||
| { | ||
| const storage = new AsyncLocalStorage(); | ||
|
|
||
| assert.strictEqual(storage.getStore(), undefined); | ||
|
|
||
| { | ||
| using scope = storage.withScope('test'); | ||
| assert.strictEqual(storage.getStore(), 'test'); | ||
| } | ||
|
|
||
| // Store should be restored to undefined | ||
| assert.strictEqual(storage.getStore(), undefined); | ||
| } | ||
|
|
||
| // Test RunScope restores previous value | ||
| { | ||
| const storage = new AsyncLocalStorage(); | ||
|
|
||
| storage.enterWith('initial'); | ||
| assert.strictEqual(storage.getStore(), 'initial'); | ||
|
|
||
| { | ||
| using scope = storage.withScope('scoped'); | ||
| assert.strictEqual(storage.getStore(), 'scoped'); | ||
| } | ||
|
|
||
| // Should restore to previous value | ||
| assert.strictEqual(storage.getStore(), 'initial'); | ||
| } | ||
|
|
||
| // Test nested RunScope | ||
| { | ||
| const storage = new AsyncLocalStorage(); | ||
| const storeValues = []; | ||
|
|
||
| { | ||
| using outer = storage.withScope('outer'); | ||
| storeValues.push(storage.getStore()); | ||
|
|
||
| { | ||
| using inner = storage.withScope('inner'); | ||
| storeValues.push(storage.getStore()); | ||
| } | ||
|
|
||
| // Should restore to outer | ||
| storeValues.push(storage.getStore()); | ||
| } | ||
|
|
||
| // Should restore to undefined | ||
| storeValues.push(storage.getStore()); | ||
|
|
||
| assert.deepStrictEqual(storeValues, ['outer', 'inner', 'outer', undefined]); | ||
| } | ||
|
|
||
| // Test RunScope with error during usage | ||
| { | ||
| const storage = new AsyncLocalStorage(); | ||
|
|
||
| storage.enterWith('before'); | ||
|
|
||
| const testError = new Error('test'); | ||
|
|
||
| assert.throws(() => { | ||
| using scope = storage.withScope('during'); | ||
| assert.strictEqual(storage.getStore(), 'during'); | ||
| throw testError; | ||
| }, testError); | ||
|
|
||
| // Store should be restored even after error | ||
| assert.strictEqual(storage.getStore(), 'before'); | ||
| } | ||
|
|
||
| // Test idempotent disposal | ||
| { | ||
| const storage = new AsyncLocalStorage(); | ||
|
|
||
| const scope = storage.withScope('test'); | ||
| assert.strictEqual(storage.getStore(), 'test'); | ||
|
|
||
| // Dispose via Symbol.dispose | ||
| scope[Symbol.dispose](); | ||
| assert.strictEqual(storage.getStore(), undefined); | ||
|
|
||
| // Double dispose should be idempotent | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe adapt to following to better verify that second call does nothing: |
||
| scope[Symbol.dispose](); | ||
| assert.strictEqual(storage.getStore(), undefined); | ||
| } | ||
|
|
||
| // Test RunScope with defaultValue | ||
| { | ||
| const storage = new AsyncLocalStorage({ defaultValue: 'default' }); | ||
|
|
||
| assert.strictEqual(storage.getStore(), 'default'); | ||
|
|
||
| { | ||
| using scope = storage.withScope('custom'); | ||
| assert.strictEqual(storage.getStore(), 'custom'); | ||
| } | ||
|
|
||
| // Should restore to default | ||
| assert.strictEqual(storage.getStore(), 'default'); | ||
| } | ||
|
|
||
| // Test deeply nested RunScope | ||
| { | ||
| const storage = new AsyncLocalStorage(); | ||
|
|
||
| { | ||
| using s1 = storage.withScope(1); | ||
| assert.strictEqual(storage.getStore(), 1); | ||
|
|
||
| { | ||
| using s2 = storage.withScope(2); | ||
| assert.strictEqual(storage.getStore(), 2); | ||
|
|
||
| { | ||
| using s3 = storage.withScope(3); | ||
| assert.strictEqual(storage.getStore(), 3); | ||
|
|
||
| { | ||
| using s4 = storage.withScope(4); | ||
| assert.strictEqual(storage.getStore(), 4); | ||
| } | ||
|
|
||
| assert.strictEqual(storage.getStore(), 3); | ||
| } | ||
|
|
||
| assert.strictEqual(storage.getStore(), 2); | ||
| } | ||
|
|
||
| assert.strictEqual(storage.getStore(), 1); | ||
| } | ||
|
|
||
| assert.strictEqual(storage.getStore(), undefined); | ||
| } | ||
|
|
||
| // Test RunScope with multiple storages | ||
| { | ||
| const storage1 = new AsyncLocalStorage(); | ||
| const storage2 = new AsyncLocalStorage(); | ||
|
|
||
| { | ||
| using scope1 = storage1.withScope('A'); | ||
|
|
||
| { | ||
| using scope2 = storage2.withScope('B'); | ||
|
|
||
| assert.strictEqual(storage1.getStore(), 'A'); | ||
| assert.strictEqual(storage2.getStore(), 'B'); | ||
| } | ||
|
|
||
| assert.strictEqual(storage1.getStore(), 'A'); | ||
| assert.strictEqual(storage2.getStore(), undefined); | ||
| } | ||
|
|
||
| assert.strictEqual(storage1.getStore(), undefined); | ||
| assert.strictEqual(storage2.getStore(), undefined); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.