@warlock.js/context
v4.0.133
Published
A simple and unified way to share context using AsyncLocalStorage for the Warlock.js framework
Maintainers
Readme
@warlock.js/context
A 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.
Features
- 🚀 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
Installation
npm install @warlock.js/contextyarn add @warlock.js/contextpnpm add @warlock.js/contextQuick Start
Creating a Custom Context
Extend the Context abstract class to create your own typed context:
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<UserContextStore> {
/**
* Called when contextManager executese buildStores()
*/
public buildStore(payload?: Record<string, any>): UserContextStore {
// 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);Using the Context
With run() - Scoped execution
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
// 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
}Managing Multiple Contexts
Use the global contextManager to orchestrate multiple contexts together:
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<string, any>) {
return { requestId: payload?.requestId ?? "", path: payload?.path ?? "" };
}
}
class DatabaseContext extends Context<{ dataSource: string }> {
/**
* Called when contextManager executese buildStores()
*/
public buildStore(payload?: Record<string, any>) {
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'
});API Reference
Context<TStore> (Abstract Class)
The base class for all context implementations.
Methods
| Method | Description |
| --------------------------------------------------------------- | -------------------------------------------------------- |
| run<T>(store: TStore, callback: () => Promise<T>): Promise<T> | Execute a callback within a new context scope |
| enter(store: TStore): void | Enter a context without a callback (middleware-style) |
| update(updates: Partial<TStore>): void | Merge new data into the current context |
| getStore(): TStore \| undefined | Get the entire current context store |
| get<K extends keyof TStore>(key: K): TStore[K] \| undefined | Get a specific value from context |
| set<K extends keyof TStore>(key: K, value: TStore[K]): void | Set a specific value in context |
| clear(): void | Clear the current context |
| hasContext(): boolean | Check if currently within a context |
| buildStore(payload?: Record<string, any>): TStore | Abstract - Override to provide custom initialization |
ContextManager
Orchestrates multiple contexts together.
Methods
| Method | Description |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| register(name: string, context: Context<any>): this | Register a context with a unique name |
| unregister(name: string): boolean | Remove a registered context |
| runAll<T>(stores: Record<string, any>, callback: () => Promise<T>): Promise<T> | Run all contexts together |
| enterAll(stores: Record<string, any>): void | Enter all contexts at once |
| clearAll(): void | Clear all contexts |
| buildStores(payload?: Record<string, any>): Record<string, any> | Build stores for all contexts using their buildStore() methods |
| getContext<T>(name: string): T \| undefined | Get a registered context by name |
| hasContext(name: string): boolean | Check if a context is registered |
Global Context Manager
A pre-configured singleton is exported for convenience:
import { contextManager } from "@warlock.js/context";
contextManager.register("myContext", myContextInstance);Real-World Examples
Multi-Tenant Application
import { Context, contextManager } from "@warlock.js/context";
interface TenantStore {
tenantId: string;
tenantName: string;
config: Record<string, any>;
}
class TenantContext extends Context<TenantStore> {
/**
* Called when contextManager executese buildStores()
*/
public buildStore(payload?: Record<string, any>): TenantStore {
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);
}Request Tracing
import { Context } from "@warlock.js/context";
import { randomUUID } from "crypto";
interface TraceStore {
traceId: string;
spanId: string;
startTime: number;
}
class TraceContext extends Context<TraceStore> {
/**
* 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}`);
await next();
traceContext.log(
`Request completed in ${Date.now() - stores.trace.startTime}ms`
);
});
});Combining with ContextManager
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();
});
});Requirements
- Node.js >= 18.0.0
- TypeScript >= 5.0 (for development)
License
MIT © hassanzohdy
Related Packages
- @warlock.js/core - Core Warlock.js framework
- Warlock.js - Full-featured Node.js framework
