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: https://github.com/nodejs/node/pull/57766
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
James M Snell 2025-04-05 13:28:51 -07:00
parent 4c5341ab73
commit a369818b1e
5 changed files with 159 additions and 3 deletions

View File

@ -116,13 +116,16 @@ Each instance of `AsyncLocalStorage` maintains an independent storage context.
Multiple instances can safely exist simultaneously without risk of interfering
with each other's data.
### `new AsyncLocalStorage()`
### `new AsyncLocalStorage([options])`
<!-- YAML
added:
- v13.10.0
- v12.17.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/57766
description: Add `defaultValue` and `name` options.
- version:
- v19.7.0
- v18.16.0
@ -135,6 +138,10 @@ changes:
description: Add option onPropagate.
-->
* `options` {Object}
* `defaultValue` {any} The default value to be used when no store is provided.
* `name` {string} A name for the `AsyncLocalStorage` value.
Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
`run()` call or after an `enterWith()` call.
@ -286,6 +293,16 @@ emitter.emit('my-event');
asyncLocalStorage.getStore(); // Returns the same object
```
### `asyncLocalStorage.name`
<!-- YAML
added: REPLACEME
-->
* {string}
The name of the `AsyncLocalStorage` instance if provided.
### `asyncLocalStorage.run(store, callback[, ...args])`
<!-- YAML

View File

@ -4,10 +4,37 @@ const {
ReflectApply,
} = primordials;
const {
validateObject,
} = require('internal/validators');
const AsyncContextFrame = require('internal/async_context_frame');
const { AsyncResource } = require('async_hooks');
class AsyncLocalStorage {
#defaultValue = undefined;
#name = undefined;
/**
* @typedef {object} AsyncLocalStorageOptions
* @property {any} [defaultValue] - The default value to use when no value is set.
* @property {string} [name] - The name of the storage.
*/
/**
* @param {AsyncLocalStorageOptions} [options]
*/
constructor(options = {}) {
validateObject(options, 'options');
this.#defaultValue = options.defaultValue;
if (options.name !== undefined) {
this.#name = `${options.name}`;
}
}
/** @type {string} */
get name() { return this.#name || ''; }
static bind(fn) {
return AsyncResource.bind(fn);
}
@ -40,7 +67,11 @@ class AsyncLocalStorage {
}
getStore() {
return AsyncContextFrame.current()?.get(this);
const frame = AsyncContextFrame.current();
if (!frame?.has(this)) {
return this.#defaultValue;
}
return frame?.get(this);
}
}

View File

@ -9,6 +9,10 @@ const {
Symbol,
} = primordials;
const {
validateObject,
} = require('internal/validators');
const {
AsyncResource,
createHook,
@ -27,11 +31,31 @@ const storageHook = createHook({
});
class AsyncLocalStorage {
constructor() {
#defaultValue = undefined;
#name = undefined;
/**
* @typedef {object} AsyncLocalStorageOptions
* @property {any} [defaultValue] - The default value to use when no value is set.
* @property {string} [name] - The name of the storage.
*/
/**
* @param {AsyncLocalStorageOptions} [options]
*/
constructor(options = {}) {
this.kResourceStore = Symbol('kResourceStore');
this.enabled = false;
validateObject(options, 'options');
this.#defaultValue = options.defaultValue;
if (options.name !== undefined) {
this.#name = `${options.name}`;
}
}
/** @type {string} */
get name() { return this.#name || ''; }
static bind(fn) {
return AsyncResource.bind(fn);
}
@ -109,8 +133,12 @@ class AsyncLocalStorage {
getStore() {
if (this.enabled) {
const resource = executionAsyncResource();
if (!(this.kResourceStore in resource)) {
return this.#defaultValue;
}
return resource[this.kResourceStore];
}
return this.#defaultValue;
}
}

View File

@ -0,0 +1,40 @@
// Flags: --no-async-context-frame
'use strict';
require('../common');
const {
AsyncLocalStorage,
} = require('async_hooks');
const {
strictEqual,
throws,
} = require('assert');
// ============================================================================
// The defaultValue option
const als1 = new AsyncLocalStorage();
strictEqual(als1.getStore(), undefined, 'value should be undefined');
const als2 = new AsyncLocalStorage({ defaultValue: 'default' });
strictEqual(als2.getStore(), 'default', 'value should be "default"');
const als3 = new AsyncLocalStorage({ defaultValue: 42 });
strictEqual(als3.getStore(), 42, 'value should be 42');
const als4 = new AsyncLocalStorage({ defaultValue: null });
strictEqual(als4.getStore(), null, 'value should be null');
throws(() => new AsyncLocalStorage(null), {
code: 'ERR_INVALID_ARG_TYPE',
});
// ============================================================================
// The name option
const als5 = new AsyncLocalStorage({ name: 'test' });
strictEqual(als5.name, 'test');
const als6 = new AsyncLocalStorage();
strictEqual(als6.name, '');

View File

@ -0,0 +1,40 @@
// Flags: --async-context-frame
'use strict';
require('../common');
const {
AsyncLocalStorage,
} = require('async_hooks');
const {
strictEqual,
throws,
} = require('assert');
// ============================================================================
// The defaultValue option
const als1 = new AsyncLocalStorage();
strictEqual(als1.getStore(), undefined, 'value should be undefined');
const als2 = new AsyncLocalStorage({ defaultValue: 'default' });
strictEqual(als2.getStore(), 'default', 'value should be "default"');
const als3 = new AsyncLocalStorage({ defaultValue: 42 });
strictEqual(als3.getStore(), 42, 'value should be 42');
const als4 = new AsyncLocalStorage({ defaultValue: null });
strictEqual(als4.getStore(), null, 'value should be null');
throws(() => new AsyncLocalStorage(null), {
code: 'ERR_INVALID_ARG_TYPE',
});
// ============================================================================
// The name option
const als5 = new AsyncLocalStorage({ name: 'test' });
strictEqual(als5.name, 'test');
const als6 = new AsyncLocalStorage();
strictEqual(als6.name, '');