A simple and unified way to share context using AsyncLocalStorage for the Warlock.js framework
npm install @warlock.js/contextA lightweight, type-safe context management library built on Node.js's AsyncLocalStorage. Provides a simple and unified way to share context across async operations in your applications.
Part of the Warlock.js ecosystem.
- 🚀 Simple API - Intuitive methods for context management
- 🔒 Type-safe - Full TypeScript support with generics
- 🔗 Chainable - Fluent API for registering multiple contexts
- 🎯 Extensible - Abstract base class for custom context implementations
- 📦 Zero dependencies - Only uses Node.js built-in AsyncLocalStorage
- 🔄 Multi-context support - Orchestrate multiple contexts together
``bash`
npm install @warlock.js/context
`bash`
yarn add @warlock.js/context
`bash`
pnpm add @warlock.js/context
Extend the Context abstract class to create your own typed context:
`typescript
import { Context, contextManager } from "@warlock.js/context";
// Define your store type
interface UserContextStore {
userId: string;
role: "admin" | "user";
tenantId: string;
}
// Create your context class
class UserContext extends Context
/**
* Called when contextManager executese buildStores()
*/
public buildStore(payload?: Record
// Initialize from payload or defaults
return {
userId: payload?.userId ?? "",
role: payload?.role ?? "user",
tenantId: payload?.tenantId ?? "",
};
}
}
// Create a singleton instance
export const userContext = new UserContext();
// register it in the context manager
contextManager.register("user", userContext);
`
#### With run() - Scoped execution
`typescript
await userContext.run(
{ userId: "123", role: "admin", tenantId: "acme" },
async () => {
// Context is available throughout this async scope
const userId = userContext.get("userId"); // '123'
const role = userContext.get("role"); // 'admin'
await someAsyncOperation(); // Context propagates through async calls
}
);
`
#### With enter() - Middleware-style
`typescript
// In your middleware
function authMiddleware(req, res, next) {
userContext.enter({
userId: req.user.id,
role: req.user.role,
tenantId: req.headers["x-tenant-id"],
});
next(); // Context is available for the rest of the request
}
`
Use the global contextManager to orchestrate multiple contexts together:
`typescript
import { Context, contextManager } from "@warlock.js/context";
// Define your contexts
class RequestContext extends Context<{ requestId: string; path: string }> {
/**
* Called when contextManager executese buildStores()
*/
public buildStore(payload?: Record
return { requestId: payload?.requestId ?? "", path: payload?.path ?? "" };
}
}
class DatabaseContext extends Context<{ dataSource: string }> {
/**
* Called when contextManager executese buildStores()
*/
public buildStore(payload?: Record
return { dataSource: payload?.dataSource ?? "primary" };
}
}
// Create instances and register them immediately
export const requestContext = new RequestContext();
contextManager.register("request", requestContext);
export const databaseContext = new DatabaseContext();
contextManager.register("database", databaseContext);
// Build stores first - each context's buildStore() is called with the payload
const stores = contextManager.buildStores({
requestId: "req-123",
path: "/api/users",
dataSource: "replica",
});
// Run all contexts together
await contextManager.runAll(stores, async () => {
// All contexts are active!
const reqId = requestContext.get("requestId"); // 'req-123'
const ds = databaseContext.get("dataSource"); // 'replica'
});
`
The base class for all context implementations.
#### Methods
| Method | Description |
| --------------------------------------------------------------- | -------------------------------------------------------- |
| run | Execute a callback within a new context scope |enter(store: TStore): void
| | Enter a context without a callback (middleware-style) |update(updates: Partial
| | Merge new data into the current context |getStore(): TStore \| undefined
| | Get the entire current context store |get
| | Get a specific value from context |set
| | Set a specific value in context |clear(): void
| | Clear the current context |hasContext(): boolean
| | Check if currently within a context |buildStore(payload?: Record
| | Abstract - Override to provide custom initialization |
Orchestrates multiple contexts together.
#### Methods
| Method | Description |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| register(name: string, context: Context | Register a context with a unique name |unregister(name: string): boolean
| | Remove a registered context |runAll
| | Run all contexts together |enterAll(stores: Record
| | Enter all contexts at once |clearAll(): void
| | Clear all contexts |buildStores(payload?: Record
| | Build stores for all contexts using their buildStore() methods |getContext
| | Get a registered context by name |hasContext(name: string): boolean
| | Check if a context is registered |
A pre-configured singleton is exported for convenience:
`typescript
import { contextManager } from "@warlock.js/context";
contextManager.register("myContext", myContextInstance);
`
`typescript
import { Context, contextManager } from "@warlock.js/context";
interface TenantStore {
tenantId: string;
tenantName: string;
config: Record
}
class TenantContext extends Context
/**
* Called when contextManager executese buildStores()
*/
public buildStore(payload?: Record
return {
tenantId: payload?.tenantId ?? "",
tenantName: payload?.tenantName ?? "",
config: payload?.config ?? {},
};
}
// Convenience getters
public get tenantId() {
return this.get("tenantId");
}
public get config() {
return this.get("config");
}
}
export const tenantContext = new TenantContext();
// In your middleware
app.use(async (req, res, next) => {
const tenant = await getTenantFromRequest(req);
tenantContext.enter({
tenantId: tenant.id,
tenantName: tenant.name,
config: tenant.config,
});
next();
});
// Anywhere in your application
function getDatabaseConnection() {
const tenantId = tenantContext.tenantId;
return getConnectionForTenant(tenantId);
}
`
`typescript
import { Context } from "@warlock.js/context";
import { randomUUID } from "crypto";
interface TraceStore {
traceId: string;
spanId: string;
startTime: number;
}
class TraceContext extends Context
/**
* Called when contextManager executese buildStores()
*/
public buildStore(): TraceStore {
return {
traceId: randomUUID(),
spanId: randomUUID(),
startTime: Date.now(),
};
}
public get traceId() {
return this.get("traceId");
}
public log(message: string) {
const store = this.getStore();
console.log([${store?.traceId}] ${message});
}
}
export const traceContext = new TraceContext();
// Usage
app.use((req, res, next) => {
const stores = { trace: traceContext.buildStore() };
traceContext.run(stores.trace, async () => {
traceContext.log(Request started: ${req.path});Request completed in ${Date.now() - stores.trace.startTime}ms
await next();
traceContext.log(
`
);
});
});
`typescript
import { contextManager } from "@warlock.js/context";
import { requestContext } from "./request-context";
import { traceContext } from "./trace-context";
import { tenantContext } from "./tenant-context";
// Register all contexts at app startup
contextManager
.register("request", requestContext)
.register("trace", traceContext)
.register("tenant", tenantContext);
// In your request handler
app.use(async (req, res, next) => {
// Build all stores from the request payload
const stores = contextManager.buildStores({
request: req,
response: res,
tenantId: req.headers["x-tenant-id"],
});
// Run all contexts together
await contextManager.runAll(stores, async () => {
await next();
});
});
``
- Node.js >= 18.0.0
- TypeScript >= 5.0 (for development)
MIT © hassanzohdy
- @warlock.js/core - Core Warlock.js framework
- Warlock.js - Full-featured Node.js framework