node/test/parallel/test-async-local-storage-run-scope.js
Stephen Belanger 5b6091ce31
async_hooks: add using scopes to AsyncLocalStorage
Adds support for using scope = storage.withScope(data) to do
the equivalent of a storage.run(data, fn) with using syntax.
This enables avoiding unnecessary closures.

PR-URL: https://github.com/nodejs/node/pull/61674
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
Reviewed-By: Bryan English <bryan@bryanenglish.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2026-03-28 20:53:57 +00:00

202 lines
4.8 KiB
JavaScript

/* eslint-disable no-unused-vars */
'use strict';
require('../common');
const assert = require('node:assert');
const { AsyncLocalStorage } = require('node:async_hooks');
// Test basic RunScope with using
{
const storage = new AsyncLocalStorage();
assert.strictEqual(storage.getStore(), undefined);
{
using scope = storage.withScope('test');
assert.strictEqual(storage.getStore(), 'test');
}
// Store should be restored to undefined
assert.strictEqual(storage.getStore(), undefined);
}
// Test RunScope restores previous value
{
const storage = new AsyncLocalStorage();
storage.enterWith('initial');
assert.strictEqual(storage.getStore(), 'initial');
{
using scope = storage.withScope('scoped');
assert.strictEqual(storage.getStore(), 'scoped');
}
// Should restore to previous value
assert.strictEqual(storage.getStore(), 'initial');
}
// Test nested RunScope
{
const storage = new AsyncLocalStorage();
const storeValues = [];
{
using outer = storage.withScope('outer');
storeValues.push(storage.getStore());
{
using inner = storage.withScope('inner');
storeValues.push(storage.getStore());
}
// Should restore to outer
storeValues.push(storage.getStore());
}
// Should restore to undefined
storeValues.push(storage.getStore());
assert.deepStrictEqual(storeValues, ['outer', 'inner', 'outer', undefined]);
}
// Test RunScope with error during usage
{
const storage = new AsyncLocalStorage();
storage.enterWith('before');
const testError = new Error('test');
assert.throws(() => {
using scope = storage.withScope('during');
assert.strictEqual(storage.getStore(), 'during');
throw testError;
}, testError);
// Store should be restored even after error
assert.strictEqual(storage.getStore(), 'before');
}
// Test idempotent disposal via named dispose() method
{
const storage = new AsyncLocalStorage();
const scope = storage.withScope('test');
assert.strictEqual(storage.getStore(), 'test');
// Dispose via named dispose() method
scope.dispose();
assert.strictEqual(storage.getStore(), undefined);
storage.enterWith('test2');
assert.strictEqual(storage.getStore(), 'test2');
// Double dispose should be idempotent
scope.dispose();
assert.strictEqual(storage.getStore(), 'test2');
}
// Test withScope without using keyword (scope leaks until manually disposed)
{
const storage = new AsyncLocalStorage();
const scope = storage.withScope('leaked');
assert.strictEqual(storage.getStore(), 'leaked');
// Without using, the scope persists
assert.strictEqual(storage.getStore(), 'leaked');
// Must manually dispose via named method
scope.dispose();
assert.strictEqual(storage.getStore(), undefined);
}
// Test that dispose undoes enterWith called inside scope
{
const storage = new AsyncLocalStorage();
storage.enterWith('store1');
assert.strictEqual(storage.getStore(), 'store1');
{
using _ = storage.withScope('store2');
assert.strictEqual(storage.getStore(), 'store2');
storage.enterWith('store3');
assert.strictEqual(storage.getStore(), 'store3');
}
// Restores to store1, undoing both withScope and the enterWith inside scope
assert.strictEqual(storage.getStore(), 'store1');
}
// Test RunScope with defaultValue
{
const storage = new AsyncLocalStorage({ defaultValue: 'default' });
assert.strictEqual(storage.getStore(), 'default');
{
using scope = storage.withScope('custom');
assert.strictEqual(storage.getStore(), 'custom');
}
// Should restore to default
assert.strictEqual(storage.getStore(), 'default');
}
// Test deeply nested RunScope
{
const storage = new AsyncLocalStorage();
{
using s1 = storage.withScope(1);
assert.strictEqual(storage.getStore(), 1);
{
using s2 = storage.withScope(2);
assert.strictEqual(storage.getStore(), 2);
{
using s3 = storage.withScope(3);
assert.strictEqual(storage.getStore(), 3);
{
using s4 = storage.withScope(4);
assert.strictEqual(storage.getStore(), 4);
}
assert.strictEqual(storage.getStore(), 3);
}
assert.strictEqual(storage.getStore(), 2);
}
assert.strictEqual(storage.getStore(), 1);
}
assert.strictEqual(storage.getStore(), undefined);
}
// Test RunScope with multiple storages
{
const storage1 = new AsyncLocalStorage();
const storage2 = new AsyncLocalStorage();
{
using scope1 = storage1.withScope('A');
{
using scope2 = storage2.withScope('B');
assert.strictEqual(storage1.getStore(), 'A');
assert.strictEqual(storage2.getStore(), 'B');
}
assert.strictEqual(storage1.getStore(), 'A');
assert.strictEqual(storage2.getStore(), undefined);
}
assert.strictEqual(storage1.getStore(), undefined);
assert.strictEqual(storage2.getStore(), undefined);
}