A production-ready TypeScript library that provides a shared, injectable request-scoped runtime context. Primarily backed by Node.js AsyncLocalStorage, but designed to be framework-agnostic and safe for environments where ALS is unavailable (like Edge runtimes).
In complex applications, multiple libraries or modules might need access to request-scoped data (like trace IDs, user information, etc.). If each library creates its own AsyncLocalStorage instance, they won't share data, leading to a "split-brain" problem.
This package provides a single, shared storage instance that can be injected into or shared across multiple libraries.
npm install @oamm/runtime-contextimport { runWithContext, getContext, setContext } from '@oamm/runtime-context';
const myContext = { requestId: '123' };
runWithContext(myContext, () => {
const ctx = getContext(); // { requestId: '123' }
setContext('userId', 'abc');
// ctx is now { requestId: '123', userId: 'abc' }
});The library supports three distinct scenarios for managing context, giving you flexibility depending on your needs.
Use this when you want a clean, Map-based storage for various pieces of data. This allows you to use setContext and getContext immediately with keys.
import { runWithContext, setContext, getContext } from '@oamm/runtime-context';
runWithContext(() => {
setContext('requestId', '123');
// ...
const id = getContext('requestId'); // '123'
});Use this when you have multiple independent contexts (e.g., a Database context and a User context) and you want to keep them separated without mixing.
import { runWithContext, getContext } from '@oamm/runtime-context';
const dbCtx = { connection: '...' };
const userCtx = { id: 'abc' };
runWithContext('db', dbCtx, () => {
runWithContext('user', userCtx, () => {
const db = getContext('db'); // { connection: '...' }
const user = getContext('user'); // { id: 'abc' }
// getContext() returns the whole Map containing both
});
});Use this for simple scenarios where you just need a single object as context and want to avoid the overhead of a Map.
import { runWithContext, getContext, setContext } from '@oamm/runtime-context';
const myContext = { requestId: '123' };
runWithContext(myContext, () => {
const ctx = getContext(); // { requestId: '123' }
setContext('userId', 'abc');
// myContext is now { requestId: '123', userId: 'abc' }
});The library automatically manages the underlying storage structure based on how you initialize your context. Understanding the distinction between the Store and Context Objects is key:
- Store: The top-level container held by
AsyncLocalStorage. It is either aMap(Scenarios 1 & 2) or a Raw Object (Scenario 3). - Context Object: An object stored inside the Map (Scenario 2) or the Store itself if it's a Raw Object.
1. Map-based Store (Scenarios 1 & 2)
ALS Store (Map)
│
├── 'db' ──> { connection: '...' } (Keyed Context)
├── 'user' ──> { id: 'abc' } (Keyed Context)
└── [Default] ──> { requestId: '123' } (Default Fallback Context)
2. Raw Object Store (Scenario 3)
ALS Store (Object)
│
└── { requestId: '123' }
If you are building a library that depends on this one, you can allow users to inject their own storage to ensure consistency:
import { initRuntimeContext } from '@oamm/runtime-context';
// In your app entry point
initRuntimeContext({ storage: mySharedStorage });You can enable internal debug logging to gain visibility into context operations (context entry, retrieval, and modifications). This is useful for troubleshooting context availability or state changes.
import { initRuntimeContext } from '@oamm/runtime-context';
initRuntimeContext({
debug: true
});When enabled, the library logs detailed information to console.debug prefixed with [runtime-context].
Ensures a context exists without double-wrapping, reusing the existing one if available. It supports the same three scenarios:
Ensures that a Map-based storage is available.
import { ensureContext, setItem } from '@oamm/runtime-context';
await ensureContext(async () => {
setItem('traceId', 'abc');
});Ensures a specific key exists within a Map-based context.
await ensureContext('my-key', () => ({ data: 1 }), () => {
// If 'my-key' already exists, it is reused.
});Ensures a specific context object exists. No Map is created if it's missing.
await ensureContext(() => ({ requestId: '123' }), async () => {
// If a context already existed, it is reused.
// Otherwise, a new object context is created for this scope.
});If you are building a library that needs access to the request context, simply import getContext from this package.
// my-library.ts
import { getContext } from '@oamm/runtime-context';
export function myLibraryFunction() {
const ctx = getContext();
// ... do something with ctx ...
}As long as the main application uses @oamm/runtime-context, your library will automatically have access to the same context.
This works even if your library is bundled separately, thanks to our use of a global storage key.
initRuntimeContext(config): Initializes the global storage and configuration (e.g.,debugmode,storageinjection).getContext<T>(key?): Returns the current context orundefined. Supports automatic type inference when using classes as keys.requireContext<T>(key?): Returns the current context or throws.runWithContext(fn): Runsfnwith an automatically initialized Map-based context (Scenario 1).runWithContext(key, ctx, fn): Runsfnwith a specific key in a Map-based multi-context (Scenario 2).runWithContext(ctx, fn): Runsfnwithin the given context, explicitly avoiding Map creation (Scenario 3).ensureContext(fn): Ensures a Map-based context exists (Scenario 1).ensureContext(key, create, fn): Ensures a specific keyed context exists in a Map (Scenario 2).ensureContext(create, fn): Ensures a context exists, avoiding Map creation if it needs to be created (Scenario 3).
When you need to manage multiple independent contexts (e.g., a TokenKit and a Session), you can use keys (strings, symbols, or classes). The library automatically handles Map creation and clones the parent Map when nesting contexts to ensure changes in a nested scope do not leak back to the parent.
class TokenKit {
constructor(public token: string) {}
}
const tk = new TokenKit('abc');
runWithContext(TokenKit, tk, () => {
// Type is automatically inferred as TokenKit | undefined
const context = getContext(TokenKit);
});If you need a reference to a context accessor (e.g., for dependency injection), use an arrow function:
// For classes:
const getSessionContext = () => getContext(SessionContext);
// For interfaces or default context (uses the whole store):
interface MyContext { user: string }
const getMyContext = () => getContext<MyContext>();
// For specific keys with interfaces:
const getSpecific = () => getContext<MyContext>('my-key');
// Later, call them without arguments
const session = getSessionContext(); // inferred as SessionContext | undefinedThe library provides several helpers to interact with the context. There is an important distinction between interacting with the Store (Map or Object) and interacting with Properties within a context object.
setContext(key, value): Directly interacts with the top-level ALS store.- Map-based: Performs
map.set(key, value). - Raw Object: Performs
object[key] = value.
- Map-based: Performs
getContext(key?): Retrieves a value from the ALS store.requireContext(key?): Retrieves a value from the ALS store or throws an error.
These helpers are designed to work with properties inside context objects, and they automatically handle the "Default Context Fallback".
setValue(key, value, contextKey?): Sets a property on a context object.- If
contextKeyis provided, it targets the object stored under that key in the Map. - If not provided, it targets the Default Context object. If no default object exists, it falls back to
setContext(key, value).
- If
getValue(key, contextKey?): Gets a property from a context object.- If
contextKeyis provided, it targets the object at that key. - If not provided, it first checks the Map for
key. If not found, it falls back to checking the Default Context object for that property.
- If
mergeContext(partial, contextKey?): Merges an object into a context object (targets the Default Context if nocontextKeyis provided).
When transitioning from a Raw Object context (Scenario 3) to a Map-based context (Scenarios 1 & 2), the library automatically preserves the parent object as a "default" context.
const globalContext = { traceId: '123' };
runWithContext(globalContext, () => {
// We are in Scenario 3 (Raw Object)
runWithContext('user', { id: 'abc' }, () => {
// We are now in Scenario 2 (Map-based)
// The Map contains: { 'user' => { id: 'abc' }, [Default] => globalContext }
const traceId = getValue('traceId'); // '123' (fallback to default context)
const userId = getValue('id', 'user'); // 'abc' (from 'user' keyed context)
setValue('newVal', 'foo'); // Sets globalContext.newVal = 'foo'
});
});setItem(key, value)getItem(key)
- Node.js: Automatically uses
AsyncLocalStorage. - Edge/Other: You must inject an
AsyncContextStorageimplementation or useinitRuntimeContext({ storage })if you want to share storage across libraries. By default, it will throw an error if you try torunwithout a valid storage.
Use resetForTests() in beforeEach to ensure test isolation.
import { resetForTests } from '@oamm/runtime-context';
beforeEach(() => {
resetForTests();
});