Skip to content

Commit b055801

Browse files
committed
fixup! async_hooks: add using scopes to AsyncLocalStorage
1 parent e9786c4 commit b055801

File tree

3 files changed

+85
-5
lines changed

3 files changed

+85
-5
lines changed

doc/api/async_context.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,48 @@ with JavaScript's `using` syntax.
540540
The scope automatically restores the previous store value when the `using` block
541541
exits, whether through normal completion or by throwing an error.
542542

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+
543585
## Class: `AsyncResource`
544586

545587
<!-- YAML

lib/internal/async_local_storage/run_scope.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ class RunScope {
1515
storage.enterWith(store);
1616
}
1717

18-
[SymbolDispose]() {
18+
dispose() {
1919
if (this.#disposed) {
2020
return;
2121
}
2222
this.#disposed = true;
2323
this.#storage.enterWith(this.#previousStore);
2424
}
25+
26+
[SymbolDispose]() {
27+
this.dispose();
28+
}
2529
}
2630

2731
module.exports = RunScope;

test/parallel/test-async-local-storage-run-scope.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,25 +77,59 @@ const { AsyncLocalStorage } = require('node:async_hooks');
7777
assert.strictEqual(storage.getStore(), 'before');
7878
}
7979

80-
// Test idempotent disposal
80+
// Test idempotent disposal via named dispose() method
8181
{
8282
const storage = new AsyncLocalStorage();
8383

8484
const scope = storage.withScope('test');
8585
assert.strictEqual(storage.getStore(), 'test');
8686

87-
// Dispose via Symbol.dispose
88-
scope[Symbol.dispose]();
87+
// Dispose via named dispose() method
88+
scope.dispose();
8989
assert.strictEqual(storage.getStore(), undefined);
9090

9191
storage.enterWith('test2');
9292
assert.strictEqual(storage.getStore(), 'test2');
9393

9494
// Double dispose should be idempotent
95-
scope[Symbol.dispose]();
95+
scope.dispose();
9696
assert.strictEqual(storage.getStore(), 'test2');
9797
}
9898

99+
// Test withScope without using keyword (scope leaks until manually disposed)
100+
{
101+
const storage = new AsyncLocalStorage();
102+
103+
const scope = storage.withScope('leaked');
104+
assert.strictEqual(storage.getStore(), 'leaked');
105+
106+
// Without using, the scope persists
107+
assert.strictEqual(storage.getStore(), 'leaked');
108+
109+
// Must manually dispose via named method
110+
scope.dispose();
111+
assert.strictEqual(storage.getStore(), undefined);
112+
}
113+
114+
// Test that dispose undoes enterWith called inside scope
115+
{
116+
const storage = new AsyncLocalStorage();
117+
118+
storage.enterWith('store1');
119+
assert.strictEqual(storage.getStore(), 'store1');
120+
121+
{
122+
using _ = storage.withScope('store2');
123+
assert.strictEqual(storage.getStore(), 'store2');
124+
125+
storage.enterWith('store3');
126+
assert.strictEqual(storage.getStore(), 'store3');
127+
}
128+
129+
// Restores to store1, undoing both withScope and the enterWith inside scope
130+
assert.strictEqual(storage.getStore(), 'store1');
131+
}
132+
99133
// Test RunScope with defaultValue
100134
{
101135
const storage = new AsyncLocalStorage({ defaultValue: 'default' });

0 commit comments

Comments
 (0)