From 541e41af58dab2c0a250483b4c1be17390066ba0 Mon Sep 17 00:00:00 2001 From: Branislav Katreniak Date: Mon, 27 Oct 2025 16:21:38 +0100 Subject: [PATCH] feat: add setAsyncLocalStorage for external AsyncLocalStorage injection Add a global setter function to allow injecting an external AsyncLocalStorage instance. This enables sharing a single AsyncLocalStorage across multiple request sources in applications that handle requests from various origins. Use cases: - Fastify HTTP requests - Queue consumers (e.g., RabbitMQ, SQS) - Scheduled tasks and timers - Other HTTP servers or frameworks - WebSocket connections Previously, the plugin sticked to its own AsyncLocalStorage instance, making it difficult to maintain a unified request context across different entry points. By providing setAsyncLocalStorage(), users can now create a central AsyncLocalStorage instance and share it across all request handlers, without coupling non-HTTP code to Fastify. Usage: ``` const sharedALS = new AsyncLocalStorage() setAsyncLocalStorage(sharedALS) app.register(fastifyRequestContext) ``` --- README.md | 28 +++++++++++++++ index.js | 7 +++- test-tap/requestContextPlugin.e2e.test.js | 42 +++++++++++++++++++++-- types/index.d.ts | 1 + 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0b081f1..2ce0c24 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,34 @@ const bar = requestContext.get('bar') If you have `"strictNullChecks": true` (or have `"strict": true`, which sets `"strictNullChecks": true`) in your TypeScript configuration, you will notice that the type of the returned value can still be `undefined` even though the `RequestContextData` interface has a specific type. For a discussion about how to work around this and the pros/cons of doing so, please read [this issue (#93)](https://github.com/fastify/fastify-request-context/issues/93). +## Sharing context across multiple request sources + +You can share a single `AsyncLocalStorage` instance across multiple request sources (HTTP, queues, scheduled tasks, etc.) by using `setAsyncLocalStorage()` before plugin registration: + +```js +const { AsyncLocalStorage } = require('node:async_hooks'); +const { fastifyRequestContext, setAsyncLocalStorage, requestContext } = require('@fastify/request-context'); + +const sharedAsyncLocalStorage = new AsyncLocalStorage(); + +// Set the global instance before plugin registration +setAsyncLocalStorage(sharedAsyncLocalStorage); + +// Register plugin +app.register(fastifyRequestContext, { + defaultStoreValues: { /* ... */ } +}); + +// Use in other parts of your application with the same context +messageQueue.consume(async (message) => { + await sharedAsyncLocalStorage.run({ messageId: message.id }, async () => { + await processMessage(message); // requestContext.get/set work here + }); +}); +``` + +This enables unified request context across different entry points in your application without coupling non-HTTP code to Fastify. + ## Usage outside of a request If functions depend on requestContext but are not called in a request, i.e. in tests or workers, they can be wrapped in the asyncLocalStorage instance of requestContext: diff --git a/index.js b/index.js index cb86172..f68bcbf 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,11 @@ const fp = require('fastify-plugin') const asyncResourceSymbol = Symbol('asyncResource') -const asyncLocalStorage = new AsyncLocalStorage() +let asyncLocalStorage = new AsyncLocalStorage() + +function setAsyncLocalStorage(it) { + asyncLocalStorage = it +} const requestContext = { get: (key) => { @@ -67,6 +71,7 @@ module.exports = fp(fastifyRequestContext, { module.exports.default = fastifyRequestContext module.exports.fastifyRequestContext = fastifyRequestContext module.exports.asyncLocalStorage = asyncLocalStorage +module.exports.setAsyncLocalStorage = setAsyncLocalStorage module.exports.requestContext = requestContext // Deprecated diff --git a/test-tap/requestContextPlugin.e2e.test.js b/test-tap/requestContextPlugin.e2e.test.js index 36e0fa5..ee46b6d 100644 --- a/test-tap/requestContextPlugin.e2e.test.js +++ b/test-tap/requestContextPlugin.e2e.test.js @@ -7,11 +7,11 @@ const { initAppPostWithAllPlugins, initAppGetWithDefaultStoreValues, } = require('../test/internal/appInitializer') -const { fastifyRequestContext } = require('..') +const { fastifyRequestContext, setAsyncLocalStorage } = require('..') const { TestService } = require('../test/internal/testService') const { test, afterEach } = require('node:test') const { CustomResource, AsyncHookContainer } = require('../test/internal/watcherService') -const { executionAsyncId } = require('node:async_hooks') +const { AsyncLocalStorage, executionAsyncId } = require('node:async_hooks') let app afterEach(() => { @@ -392,3 +392,41 @@ test('returns the store', (t) => { }) }) }) + +test('uses external AsyncLocalStorage when provided', (t) => { + t.plan(3) + + const externalALS = new AsyncLocalStorage() + setAsyncLocalStorage(externalALS) + + app = fastify({ logger: true }) + app.register(fastifyRequestContext, { + defaultStoreValues: { userId: 'default' }, + }) + + const route = (req) => { + // Set value directly on the external ALS store + const store = externalALS.getStore() + store.userId = 'test-user' + + // Verify the value is accessible through both APIs + const valueFromPlugin = req.requestContext.get('userId') + const valueFromExternalALS = store.userId + + t.assert.strictEqual(valueFromPlugin, 'test-user') + t.assert.strictEqual(valueFromExternalALS, 'test-user') + + return { userId: valueFromExternalALS } + } + + app.get('/', route) + + return app.listen({ port: 0, host: '127.0.0.1' }).then(() => { + const { address, port } = app.server.address() + const url = `${address}:${port}` + + return request('GET', url).then((response) => { + t.assert.strictEqual(response.body.userId, 'test-user') + }) + }) +}) diff --git a/types/index.d.ts b/types/index.d.ts index 303cedc..5accc14 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -55,6 +55,7 @@ declare namespace fastifyRequestContext { export const requestContext: RequestContext export const asyncLocalStorage: AsyncLocalStorage + export function setAsyncLocalStorage(als: AsyncLocalStorage): void /** * @deprecated Use FastifyRequestContextOptions instead */