This project demonstrates how to use Node.js' AsyncLocalStorage within a SvelteKit application to manage per-request data without the need for passing data manually through each load function.
In this example, the AsyncLocalStorage is initialized in the server hooks and is used to store the authenticated user and other request-specific data.
AsyncLocalStorage provides a way to store data throughout the lifetime of an asynchronous operation. It allows you to create a storage context that is unique to each asynchronous execution chain. This is particularly useful in web applications where you need to maintain request-specific data across various asynchronous operations without explicitly passing it around.
Context Creation: A new context is created at the beginning of each request using asyncLocalStorage.run()
. This context is a Map object that can store key-value pairs.
Setting Data: Data can be stored in the context using the setRequestData
function. This function retrieves the current context and sets the specified key-value pair.
Getting Data: Data can be retrieved from the context using the getRequestData
function. This function retrieves the current context and gets the value associated with the specified key.
Simplified Data Propagation: With AsyncLocalStorage, data such as the authenticated user is set once during the request lifecycle and then accessed anywhere within that lifecycle, eliminating the need to manually pass values through event.locals
in each load function.
Avoiding Load Function Waterfalls: Instead of using await parent()
in nested load functions, which can lead to complex chaining of asynchronous calls, AsyncLocalStorage allows direct access to the context data, thereby reducing boilerplate code.
Centralized Context Management: Managing request-specific data with AsyncLocalStorage centralizes the data access layer. This not only simplifies the code by avoiding repetitive data passing but also minimizes potential errors in distributed data flow.
The following snippet shows how the context is created and used in the handle
hook to store the authenticated user:
// src/hooks.server.ts
import { getUserFromToken } from '$lib/server/auth';
import { runWithContext, setRequestData } from '$lib/server/requestContext';
export const handle = async ({ event, resolve }) => {
return runWithContext(async () => {
const token = event.cookies.get('auth_token');
if (token) {
const user = await getUserFromToken(token);
setRequestData('user', user);
} else {
console.warn('No valid auth token found');
setRequestData('user', null);
}
return await resolve(event);
});
};
This snippet from src/lib/server/requestContext.ts
demonstrates how AsyncLocalStorage is used to maintain a per-request store:
// src/lib/server/requestContext.ts
import { AsyncLocalStorage } from 'node:async_hooks';
const asyncLocalStorage = new AsyncLocalStorage<Map<string, any>>();
export function runWithContext<T>(fn: () => T) {
return asyncLocalStorage.run(new Map(), fn);
}
export function setRequestData<T>(key: string, value: T) {
const store = asyncLocalStorage.getStore();
if (store) {
store.set(key, value);
} else {
console.error('AsyncLocalStorage store is not available');
}
}
export function getRequestData<T>(key: string): T | undefined {
const store = asyncLocalStorage.getStore();
return store ? store.get(key) : undefined;
}
The user information stored in AsyncLocalStorage can be accessed from other modules, such as in the authentication helper:
// src/lib/server/authUser.ts
import { getRequestData } from '$lib/server/requestContext';
import type { AuthUser } from '$lib/types/authUser';
export function getAuthenticatedUser() {
return getRequestData<AuthUser>('user');
}
[!WARNING] Node.js' AsyncLocalStorage is only available on the server. It cannot be used in client-side code.
pnpm install
).pnpm dev
.src/hooks.server.ts
and src/lib/server/
directories to see how AsyncLocalStorage is integrated.