Skip to content

Commit 0612c1f

Browse files
committed
lib: add defaultValue and name options to AsyncLocalStorage
The upcoming `AsyncContext` specification adds a default value and name to async storage variables. This adds the same to `AsyncLocalStorage` to promote closer alignment with the pending spec. ```js const als = new AsyncLocalStorage({ name: 'foo', defaultValue: 123, }); console.log(als.name); // 'foo' console.log(als.getStore()); // 123 ``` Refs: https://github.com/tc39/proposal-async-context/blob/master/spec.html
1 parent 214e3d4 commit 0612c1f

File tree

5 files changed

+152
-3
lines changed

5 files changed

+152
-3
lines changed

doc/api/async_context.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,16 @@ Each instance of `AsyncLocalStorage` maintains an independent storage context.
116116
Multiple instances can safely exist simultaneously without risk of interfering
117117
with each other's data.
118118

119-
### `new AsyncLocalStorage()`
119+
### `new AsyncLocalStorage([options])`
120120

121121
<!-- YAML
122122
added:
123123
- v13.10.0
124124
- v12.17.0
125125
changes:
126+
- version: REPLACEME
127+
pr-url: https://github.com/nodejs/node/pull/00000
128+
description: Add `defaultValue` and `name` options.
126129
- version:
127130
- v19.7.0
128131
- v18.16.0
@@ -135,6 +138,10 @@ changes:
135138
description: Add option onPropagate.
136139
-->
137140

141+
* `options` {Object}
142+
* `defaultValue` {any} The default value to be used when no store is provided.
143+
* `name` {string} A name for the `AsyncLocalStorage` value.
144+
138145
Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
139146
`run()` call or after an `enterWith()` call.
140147

@@ -286,6 +293,16 @@ emitter.emit('my-event');
286293
asyncLocalStorage.getStore(); // Returns the same object
287294
```
288295

296+
### `asyncLocalStorage.name`
297+
298+
<!-- YAML
299+
added: REPLACEME
300+
-->
301+
302+
* {string|undefined}
303+
304+
The name of the `AsyncLocalStorage` instance if provided.
305+
289306
### `asyncLocalStorage.run(store, callback[, ...args])`
290307

291308
<!-- YAML

lib/internal/async_local_storage/async_context_frame.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,29 @@ const {
44
ReflectApply,
55
} = primordials;
66

7+
const {
8+
validateObject,
9+
validateString,
10+
} = require('internal/validators');
11+
712
const AsyncContextFrame = require('internal/async_context_frame');
813
const { AsyncResource } = require('async_hooks');
914

1015
class AsyncLocalStorage {
16+
#defaultValue = undefined;
17+
#name = undefined;
18+
19+
constructor(options = {}) {
20+
validateObject(options, 'options');
21+
this.#defaultValue = options.defaultValue;
22+
23+
if (options.name !== undefined) {
24+
validateString(options.name, 'options.name');
25+
this.#name = options.name;
26+
}
27+
}
28+
29+
get name() { return this.#name; }
1130
static bind(fn) {
1231
return AsyncResource.bind(fn);
1332
}
@@ -40,7 +59,11 @@ class AsyncLocalStorage {
4059
}
4160

4261
getStore() {
43-
return AsyncContextFrame.current()?.get(this);
62+
const frame = AsyncContextFrame.current();
63+
if (!frame?.has(this)) {
64+
return this.#defaultValue;
65+
}
66+
return frame?.get(this);
4467
}
4568
}
4669

lib/internal/async_local_storage/async_hooks.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const {
99
Symbol,
1010
} = primordials;
1111

12+
const {
13+
validateObject,
14+
validateString,
15+
} = require('internal/validators');
16+
1217
const {
1318
AsyncResource,
1419
createHook,
@@ -27,11 +32,23 @@ const storageHook = createHook({
2732
});
2833

2934
class AsyncLocalStorage {
30-
constructor() {
35+
#defaultValue = undefined;
36+
#name = undefined;
37+
38+
constructor(options = {}) {
3139
this.kResourceStore = Symbol('kResourceStore');
3240
this.enabled = false;
41+
validateObject(options, 'options');
42+
this.#defaultValue = options.defaultValue;
43+
44+
if (options.name !== undefined) {
45+
validateString(options.name, 'options.name');
46+
this.#name = options.name;
47+
}
3348
}
3449

50+
get name() { return this.#name; }
51+
3552
static bind(fn) {
3653
return AsyncResource.bind(fn);
3754
}
@@ -109,8 +126,12 @@ class AsyncLocalStorage {
109126
getStore() {
110127
if (this.enabled) {
111128
const resource = executionAsyncResource();
129+
if (!(this.kResourceStore in resource)) {
130+
return this.#defaultValue;
131+
}
112132
return resource[this.kResourceStore];
113133
}
134+
return this.#defaultValue;
114135
}
115136
}
116137

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Flags: --no-async-context-frame
2+
'use strict';
3+
4+
require('../common');
5+
6+
const {
7+
AsyncLocalStorage,
8+
} = require('async_hooks');
9+
10+
const {
11+
strictEqual,
12+
throws,
13+
} = require('assert');
14+
15+
// ============================================================================
16+
// The defaultValue option
17+
const als1 = new AsyncLocalStorage();
18+
strictEqual(als1.getStore(), undefined, 'value should be undefined');
19+
20+
const als2 = new AsyncLocalStorage({ defaultValue: 'default' });
21+
strictEqual(als2.getStore(), 'default', 'value should be "default"');
22+
23+
const als3 = new AsyncLocalStorage({ defaultValue: 42 });
24+
strictEqual(als3.getStore(), 42, 'value should be 42');
25+
26+
const als4 = new AsyncLocalStorage({ defaultValue: null });
27+
strictEqual(als4.getStore(), null, 'value should be null');
28+
29+
throws(() => new AsyncLocalStorage(null), {
30+
code: 'ERR_INVALID_ARG_TYPE',
31+
});
32+
33+
// ============================================================================
34+
// The name option
35+
36+
const als5 = new AsyncLocalStorage({ name: 'test' });
37+
strictEqual(als5.name, 'test');
38+
39+
const als6 = new AsyncLocalStorage();
40+
strictEqual(als6.name, undefined);
41+
42+
throws(() => new AsyncLocalStorage({ name: 1 }), {
43+
code: 'ERR_INVALID_ARG_TYPE',
44+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Flags: --async-context-frame
2+
'use strict';
3+
4+
require('../common');
5+
6+
const {
7+
AsyncLocalStorage,
8+
} = require('async_hooks');
9+
10+
const {
11+
strictEqual,
12+
throws,
13+
} = require('assert');
14+
15+
// ============================================================================
16+
// The defaultValue option
17+
const als1 = new AsyncLocalStorage();
18+
strictEqual(als1.getStore(), undefined, 'value should be undefined');
19+
20+
const als2 = new AsyncLocalStorage({ defaultValue: 'default' });
21+
strictEqual(als2.getStore(), 'default', 'value should be "default"');
22+
23+
const als3 = new AsyncLocalStorage({ defaultValue: 42 });
24+
strictEqual(als3.getStore(), 42, 'value should be 42');
25+
26+
const als4 = new AsyncLocalStorage({ defaultValue: null });
27+
strictEqual(als4.getStore(), null, 'value should be null');
28+
29+
throws(() => new AsyncLocalStorage(null), {
30+
code: 'ERR_INVALID_ARG_TYPE',
31+
});
32+
33+
// ============================================================================
34+
// The name option
35+
36+
const als5 = new AsyncLocalStorage({ name: 'test' });
37+
strictEqual(als5.name, 'test');
38+
39+
const als6 = new AsyncLocalStorage();
40+
strictEqual(als6.name, undefined);
41+
42+
throws(() => new AsyncLocalStorage({ name: 1 }), {
43+
code: 'ERR_INVALID_ARG_TYPE',
44+
});

0 commit comments

Comments
 (0)