Skip to content

Commit 3e68ac6

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 3e68ac6

File tree

5 files changed

+161
-3
lines changed

5 files changed

+161
-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}
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: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,38 @@ 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+
/**
20+
* @typedef {object} AsyncLocalStorageOptions
21+
* @property {any} [defaultValue] - The default value to use when no value is set.
22+
* @property {string} [name] - The name of the storage.
23+
*/
24+
/**
25+
* @param {AsyncLocalStorageOptions} [options]
26+
*/
27+
constructor(options = {}) {
28+
validateObject(options, 'options');
29+
this.#defaultValue = options.defaultValue;
30+
31+
if (options.name !== undefined) {
32+
this.#name = `${options.name}`;
33+
}
34+
}
35+
36+
/** @type {string} */
37+
get name() { return this.#name || ''; }
38+
1139
static bind(fn) {
1240
return AsyncResource.bind(fn);
1341
}
@@ -40,7 +68,11 @@ class AsyncLocalStorage {
4068
}
4169

4270
getStore() {
43-
return AsyncContextFrame.current()?.get(this);
71+
const frame = AsyncContextFrame.current();
72+
if (!frame?.has(this)) {
73+
return this.#defaultValue;
74+
}
75+
return frame?.get(this);
4476
}
4577
}
4678

lib/internal/async_local_storage/async_hooks.js

Lines changed: 30 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,31 @@ const storageHook = createHook({
2732
});
2833

2934
class AsyncLocalStorage {
30-
constructor() {
35+
#defaultValue = undefined;
36+
#name = undefined;
37+
38+
/**
39+
* @typedef {object} AsyncLocalStorageOptions
40+
* @property {any} [defaultValue] - The default value to use when no value is set.
41+
* @property {string} [name] - The name of the storage.
42+
*/
43+
/**
44+
* @param {AsyncLocalStorageOptions} [options]
45+
*/
46+
constructor(options = {}) {
3147
this.kResourceStore = Symbol('kResourceStore');
3248
this.enabled = false;
49+
validateObject(options, 'options');
50+
this.#defaultValue = options.defaultValue;
51+
52+
if (options.name !== undefined) {
53+
this.#name = `${options.name}`;
54+
}
3355
}
3456

57+
/** @type {string} */
58+
get name() { return this.#name || ''; }
59+
3560
static bind(fn) {
3661
return AsyncResource.bind(fn);
3762
}
@@ -109,8 +134,12 @@ class AsyncLocalStorage {
109134
getStore() {
110135
if (this.enabled) {
111136
const resource = executionAsyncResource();
137+
if (!(this.kResourceStore in resource)) {
138+
return this.#defaultValue;
139+
}
112140
return resource[this.kResourceStore];
113141
}
142+
return this.#defaultValue;
114143
}
115144
}
116145

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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, '');
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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, '');

0 commit comments

Comments
 (0)