Skip to content

Commit c3e4434

Browse files
jasnellRafaelGSS
authored andcommitted
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 PR-URL: #57766 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]>
1 parent f99f815 commit c3e4434

File tree

5 files changed

+159
-3
lines changed

5 files changed

+159
-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/57766
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: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,37 @@ const {
44
ReflectApply,
55
} = primordials;
66

7+
const {
8+
validateObject,
9+
} = require('internal/validators');
10+
711
const AsyncContextFrame = require('internal/async_context_frame');
812
const { AsyncResource } = require('async_hooks');
913

1014
class AsyncLocalStorage {
15+
#defaultValue = undefined;
16+
#name = undefined;
17+
18+
/**
19+
* @typedef {object} AsyncLocalStorageOptions
20+
* @property {any} [defaultValue] - The default value to use when no value is set.
21+
* @property {string} [name] - The name of the storage.
22+
*/
23+
/**
24+
* @param {AsyncLocalStorageOptions} [options]
25+
*/
26+
constructor(options = {}) {
27+
validateObject(options, 'options');
28+
this.#defaultValue = options.defaultValue;
29+
30+
if (options.name !== undefined) {
31+
this.#name = `${options.name}`;
32+
}
33+
}
34+
35+
/** @type {string} */
36+
get name() { return this.#name || ''; }
37+
1138
static bind(fn) {
1239
return AsyncResource.bind(fn);
1340
}
@@ -40,7 +67,11 @@ class AsyncLocalStorage {
4067
}
4168

4269
getStore() {
43-
return AsyncContextFrame.current()?.get(this);
70+
const frame = AsyncContextFrame.current();
71+
if (!frame?.has(this)) {
72+
return this.#defaultValue;
73+
}
74+
return frame?.get(this);
4475
}
4576
}
4677

lib/internal/async_local_storage/async_hooks.js

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

12+
const {
13+
validateObject,
14+
} = require('internal/validators');
15+
1216
const {
1317
AsyncResource,
1418
createHook,
@@ -27,11 +31,31 @@ const storageHook = createHook({
2731
});
2832

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

56+
/** @type {string} */
57+
get name() { return this.#name || ''; }
58+
3559
static bind(fn) {
3660
return AsyncResource.bind(fn);
3761
}
@@ -109,8 +133,12 @@ class AsyncLocalStorage {
109133
getStore() {
110134
if (this.enabled) {
111135
const resource = executionAsyncResource();
136+
if (!(this.kResourceStore in resource)) {
137+
return this.#defaultValue;
138+
}
112139
return resource[this.kResourceStore];
113140
}
141+
return this.#defaultValue;
114142
}
115143
}
116144

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)