Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 312476c

Browse filesBrowse files
Qardaduh95
authored andcommitted
async_hooks: add using scopes to AsyncLocalStorage
Adds support for using scope = storage.withScope(data) to do the equivalent of a storage.run(data, fn) with using syntax. This enables avoiding unnecessary closures. PR-URL: #61674 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de> Reviewed-By: Bryan English <bryan@bryanenglish.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent 6c6c900 commit 312476c
Copy full SHA for 312476c

5 files changed

+409Lines changed: 409 additions & 0 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/async_context.md‎

Copy file name to clipboardExpand all lines: doc/api/async_context.md
+164Lines changed: 164 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,110 @@ try {
386386
}
387387
```
388388

389+
### `asyncLocalStorage.withScope(store)`
390+
391+
<!-- YAML
392+
added: REPLACEME
393+
-->
394+
395+
> Stability: 1 - Experimental
396+
397+
* `store` {any}
398+
* Returns: {RunScope}
399+
400+
Creates a disposable scope that enters the given store and automatically
401+
restores the previous store value when the scope is disposed. This method is
402+
designed to work with JavaScript's explicit resource management (`using` syntax).
403+
404+
Example:
405+
406+
```mjs
407+
import { AsyncLocalStorage } from 'node:async_hooks';
408+
409+
const asyncLocalStorage = new AsyncLocalStorage();
410+
411+
{
412+
using _ = asyncLocalStorage.withScope('my-store');
413+
console.log(asyncLocalStorage.getStore()); // Prints: my-store
414+
}
415+
416+
console.log(asyncLocalStorage.getStore()); // Prints: undefined
417+
```
418+
419+
```cjs
420+
const { AsyncLocalStorage } = require('node:async_hooks');
421+
422+
const asyncLocalStorage = new AsyncLocalStorage();
423+
424+
{
425+
using _ = asyncLocalStorage.withScope('my-store');
426+
console.log(asyncLocalStorage.getStore()); // Prints: my-store
427+
}
428+
429+
console.log(asyncLocalStorage.getStore()); // Prints: undefined
430+
```
431+
432+
The `withScope()` method is particularly useful for managing context in
433+
synchronous code where you want to ensure the previous store value is restored
434+
when exiting a block, even if an error is thrown.
435+
436+
```mjs
437+
import { AsyncLocalStorage } from 'node:async_hooks';
438+
439+
const asyncLocalStorage = new AsyncLocalStorage();
440+
441+
try {
442+
using _ = asyncLocalStorage.withScope('my-store');
443+
console.log(asyncLocalStorage.getStore()); // Prints: my-store
444+
throw new Error('test');
445+
} catch (e) {
446+
// Store is automatically restored even after error
447+
console.log(asyncLocalStorage.getStore()); // Prints: undefined
448+
}
449+
```
450+
451+
```cjs
452+
const { AsyncLocalStorage } = require('node:async_hooks');
453+
454+
const asyncLocalStorage = new AsyncLocalStorage();
455+
456+
try {
457+
using _ = asyncLocalStorage.withScope('my-store');
458+
console.log(asyncLocalStorage.getStore()); // Prints: my-store
459+
throw new Error('test');
460+
} catch (e) {
461+
// Store is automatically restored even after error
462+
console.log(asyncLocalStorage.getStore()); // Prints: undefined
463+
}
464+
```
465+
466+
**Important:** When using `withScope()` in async functions before the first
467+
`await`, be aware that the scope change will affect the caller's context. The
468+
synchronous portion of an async function (before the first `await`) runs
469+
immediately when called, and when it reaches the first `await`, it returns the
470+
promise to the caller. At that point, the scope change becomes visible in the
471+
caller's context and will persist in subsequent synchronous code until something
472+
else changes the scope value. For async operations, prefer using `run()` which
473+
properly isolates context across async boundaries.
474+
475+
```mjs
476+
import { AsyncLocalStorage } from 'node:async_hooks';
477+
478+
const asyncLocalStorage = new AsyncLocalStorage();
479+
480+
async function example() {
481+
using _ = asyncLocalStorage.withScope('my-store');
482+
console.log(asyncLocalStorage.getStore()); // Prints: my-store
483+
await someAsyncOperation(); // Function pauses here and returns promise
484+
console.log(asyncLocalStorage.getStore()); // Prints: my-store
485+
}
486+
487+
// Calling without await
488+
example(); // Synchronous portion runs, then pauses at first await
489+
// After the promise is returned, the scope 'my-store' is now active in caller!
490+
console.log(asyncLocalStorage.getStore()); // Prints: my-store (unexpected!)
491+
```
492+
389493
### Usage with `async/await`
390494

391495
If, within an async function, only one `await` call is to run within a context,
@@ -420,6 +524,64 @@ of `asyncLocalStorage.getStore()` after the calls you suspect are responsible
420524
for the loss. When the code logs `undefined`, the last callback called is
421525
probably responsible for the context loss.
422526

527+
## Class: `RunScope`
528+
529+
<!-- YAML
530+
added: REPLACEME
531+
-->
532+
533+
> Stability: 1 - Experimental
534+
535+
A disposable scope returned by [`asyncLocalStorage.withScope()`][] that
536+
automatically restores the previous store value when disposed. This class
537+
implements the [Explicit Resource Management][] protocol and is designed to work
538+
with JavaScript's `using` syntax.
539+
540+
The scope automatically restores the previous store value when the `using` block
541+
exits, whether through normal completion or by throwing an error.
542+
543+
### `scope.dispose()`
544+
545+
<!-- YAML
546+
added: REPLACEME
547+
-->
548+
549+
Explicitly ends the scope and restores the previous store value. This method
550+
is idempotent: calling it multiple times has the same effect as calling it once.
551+
552+
The `[Symbol.dispose]()` method defers to `dispose()`.
553+
554+
If `withScope()` is called without the `using` keyword, `dispose()` must be
555+
called manually to restore the previous store value. Forgetting to call
556+
`dispose()` will cause the store value to persist for the remainder of the
557+
current execution context:
558+
559+
```mjs
560+
import { AsyncLocalStorage } from 'node:async_hooks';
561+
562+
const storage = new AsyncLocalStorage();
563+
564+
// Without using, the scope must be disposed manually
565+
const scope = storage.withScope('my-store');
566+
// storage.getStore() === 'my-store' here
567+
568+
scope.dispose(); // Restore previous value
569+
// storage.getStore() === undefined here
570+
```
571+
572+
```cjs
573+
const { AsyncLocalStorage } = require('node:async_hooks');
574+
575+
const storage = new AsyncLocalStorage();
576+
577+
// Without using, the scope must be disposed manually
578+
const scope = storage.withScope('my-store');
579+
// storage.getStore() === 'my-store' here
580+
581+
scope.dispose(); // Restore previous value
582+
// storage.getStore() === undefined here
583+
```
584+
423585
## Class: `AsyncResource`
424586

425587
<!-- YAML
@@ -905,8 +1067,10 @@ const server = createServer((req, res) => {
9051067
}).listen(3000);
9061068
```
9071069
1070+
[Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
9081071
[`AsyncResource`]: #class-asyncresource
9091072
[`EventEmitter`]: events.md#class-eventemitter
9101073
[`Stream`]: stream.md#stream
9111074
[`Worker`]: worker_threads.md#class-worker
1075+
[`asyncLocalStorage.withScope()`]: #asynclocalstoragewithscopestore
9121076
[`util.promisify()`]: util.md#utilpromisifyoriginal
Collapse file

‎lib/internal/async_local_storage/async_context_frame.js‎

Copy file name to clipboardExpand all lines: lib/internal/async_local_storage/async_context_frame.js
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const {
1212
const AsyncContextFrame = require('internal/async_context_frame');
1313
const { AsyncResource } = require('async_hooks');
1414

15+
const RunScope = require('internal/async_local_storage/run_scope');
16+
1517
class AsyncLocalStorage {
1618
#defaultValue = undefined;
1719
#name = undefined;
@@ -77,6 +79,10 @@ class AsyncLocalStorage {
7779
}
7880
return frame?.get(this);
7981
}
82+
83+
withScope(store) {
84+
return new RunScope(this, store);
85+
}
8086
}
8187

8288
module.exports = AsyncLocalStorage;
Collapse file

‎lib/internal/async_local_storage/async_hooks.js‎

Copy file name to clipboardExpand all lines: lib/internal/async_local_storage/async_hooks.js
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const {
1919
executionAsyncResource,
2020
} = require('async_hooks');
2121

22+
const RunScope = require('internal/async_local_storage/run_scope');
23+
2224
const storageList = [];
2325
const storageHook = createHook({
2426
init(asyncId, type, triggerAsyncId, resource) {
@@ -142,6 +144,10 @@ class AsyncLocalStorage {
142144
}
143145
return this.#defaultValue;
144146
}
147+
148+
withScope(store) {
149+
return new RunScope(this, store);
150+
}
145151
}
146152

147153
module.exports = AsyncLocalStorage;
Collapse file
+31Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const {
4+
SymbolDispose,
5+
} = primordials;
6+
7+
class RunScope {
8+
#storage;
9+
#previousStore;
10+
#disposed = false;
11+
12+
constructor(storage, store) {
13+
this.#storage = storage;
14+
this.#previousStore = storage.getStore();
15+
storage.enterWith(store);
16+
}
17+
18+
dispose() {
19+
if (this.#disposed) {
20+
return;
21+
}
22+
this.#disposed = true;
23+
this.#storage.enterWith(this.#previousStore);
24+
}
25+
26+
[SymbolDispose]() {
27+
this.dispose();
28+
}
29+
}
30+
31+
module.exports = RunScope;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.