@stitchem/cls
v1.0.0
Published
AsyncLocalStorage-based continuation-local storage for Stitchem. Propagates request-scoped data (user context, request ID, trace IDs) through async call stacks without explicit parameter passing.
Downloads
1,804
Readme
@stitchem/cls
AsyncLocalStorage-based continuation-local storage for Stitchem. Propagates request-scoped data (user context, request ID, trace IDs) through async call stacks without explicit parameter passing.
Install
npm install @stitchem/cls
# or
pnpm add @stitchem/clsPeer dependencies: @stitchem/core, @stitchem/http (optional — only needed for HTTP middleware)
Setup
Register ClsModule globally in your root module. Use forRoot to populate the store on each request:
import { ClsModule, defineMiddleware } from '@stitchem/cls';
import { module } from '@stitchem/core';
@module({
imports: [
ClsModule.forRoot({
middleware: [
defineMiddleware('requestId', (ctx) =>
ctx.header('x-request-id') ?? crypto.randomUUID()
),
defineMiddleware('user', async (ctx) => {
const token = ctx.header('authorization');
return resolveUser(token);
}),
],
}),
],
})
class AppModule {}Each middleware definition receives the HttpContext and returns the value to store at the given path. Definitions run sequentially before the route handler.
Augment the Store
Extend ClsStore via declaration merging to type your store paths:
// cls.d.ts (or in your types barrel)
declare module '@stitchem/cls' {
interface ClsStore {
requestId: string;
user: {
id: number;
name: string;
};
}
}All Cls and ClsService methods will infer types from your augmentation.
Access in Route Handlers
Inject ClsService to access the store inside DI-managed classes:
import { ClsService } from '@stitchem/cls';
import { router, get } from '@stitchem/http';
@router('/users')
class UserRouter {
static inject = [ClsService] as const;
constructor(private cls: ClsService) {}
@get('/:id')
getUser() {
const requestId = this.cls.get('requestId');
const userId = this.cls.get('user.id'); // dot-notation for nested paths
return { requestId, userId };
}
}Static API
Use Cls directly in utility functions outside of the DI container:
import { Cls } from '@stitchem/cls';
function log(message: string) {
const requestId = Cls.getId() ?? 'no-context';
console.log(`[${requestId}] ${message}`);
}| Method | Description |
|--------|-------------|
| Cls.get(path) | Get value by dot-notation path. Throws if outside context. |
| Cls.set(path, value) | Set value by path. No-op if outside context. |
| Cls.has(path) | Check whether a path exists. |
| Cls.delete(path) | Delete a path from the store. |
| Cls.run(fn) | Run a function in a new context (auto-generates UUID). |
| Cls.run(id, fn) | Run with a specific context ID. |
| Cls.getId() | Returns the current context ID, or undefined. |
| Cls.isActive() | Returns true if inside an active context. |
Background Jobs
For non-HTTP entry points (queue consumers, cron jobs), create a context manually:
import { Cls } from '@stitchem/cls';
async function processJob(jobId: string) {
await Cls.run(jobId, async () => {
Cls.set('jobId', jobId);
await doWork();
});
}Cls.run() throws if called inside an existing context — it is intended for entry points only.
License
MIT
