@warlock.js/context 5.24.0 → 5.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -6,331 +6,331 @@
6
6
 
7
7
  ## define-context `@warlock.js/context/define-context/SKILL.md`
8
8
 
9
- ---
10
- name: define-context
11
- description: 'Extend Context<TStore> to define an AsyncLocalStorage-backed typed context — implement buildStore, use run / enter / update / get / set / getStore / clear / hasContext. Triggers: `Context`, `Context<TStore>`, `buildStore`, `run`, `enter`, `update`, `get`, `set`, `getStore`, `clear`, `hasContext`; "share user/tenant/trace id across async calls", "AsyncLocalStorage typed wrapper", "request-scoped store without thread-through"; typical import `import { Context } from "@warlock.js/context"`. Skip: orchestrating multiple contexts — `@warlock.js/context/orchestrate-contexts/SKILL.md`; native `AsyncLocalStorage`, `cls-hooked`, `nest-context`, React Context.'
12
- ---
13
-
14
- # Define a context
15
-
16
- `@warlock.js/context` is a tiny wrapper on Node.js's `AsyncLocalStorage`. You extend the abstract `Context<TStore>` class to declare what your context stores, and you get a typed get/set/run API that propagates through every async call inside the scope.
17
-
18
- ## Install
19
-
20
- ```bash
21
- npm install @warlock.js/context
22
- ```
23
-
24
- ## Shape
25
-
26
- ```ts
27
- import { Context } from "@warlock.js/context";
28
-
29
- interface UserContextStore {
30
- userId: string;
31
- role: "admin" | "user";
32
- tenantId: string;
33
- }
34
-
35
- class UserContext extends Context<UserContextStore> {
36
- /**
37
- * Called by `contextManager.buildStores(payload)` for each registered context.
38
- * Override to provide initialization logic for this context's store.
39
- */
40
- public buildStore(payload?: Record<string, any>): UserContextStore {
41
- return {
42
- userId: payload?.userId ?? "",
43
- role: payload?.role ?? "user",
44
- tenantId: payload?.tenantId ?? "",
45
- };
46
- }
47
- }
48
-
49
- export const userContext = new UserContext();
50
- ```
51
-
52
- `buildStore` is the **only** abstract method. Everything else is provided by `Context<TStore>`.
53
-
54
- ## Usage modes
55
-
56
- ### `run()` — scoped execution
57
-
58
- ```ts
59
- await userContext.run(
60
- { userId: "123", role: "admin", tenantId: "acme" },
61
- async () => {
62
- // Context is available throughout this async scope and any awaited calls inside it.
63
- const userId = userContext.get("userId"); // "123"
64
- const role = userContext.get("role"); // "admin"
65
-
66
- await someAsyncOperation(); // context propagates through awaits
67
- },
68
- );
69
- ```
70
-
71
- Use `run()` when you have a clear scope boundary (a request handler, a job, a CLI command). The context is auto-cleaned when the callback returns.
72
-
73
- ### `enter()` — middleware-style, no callback
74
-
75
- ```ts
76
- function authMiddleware(req, res, next) {
77
- userContext.enter({
78
- userId: req.user.id,
79
- role: req.user.role,
80
- tenantId: req.headers["x-tenant-id"],
81
- });
82
-
83
- next(); // context lives for the rest of the request
84
- }
85
- ```
86
-
87
- Use `enter()` when the framework doesn't give you a callback to wrap (Express-style middleware). Under the hood it's `AsyncLocalStorage.enterWith(store)`.
88
-
89
- ### `update()` — merge into the current context
90
-
91
- ```ts
92
- userContext.update({ role: "admin" });
93
- // existing store: { userId, role, tenantId } → { userId, role: "admin", tenantId }
94
- ```
95
-
96
- If there's no current store, `update` creates one with the partial (cast to the full type). Use for incremental enrichment as the request flows through layers.
97
-
98
- `update()` (and `set()`, which calls it) silently drops `__proto__` / `constructor` / `prototype` keys instead of merging them — merging with `Object.assign` would otherwise let a caller-shaped payload (e.g. `userContext.update(req.body)`) pollute `Object.prototype` for the whole process, since the context store is shared. The rest of the payload still merges normally; this closes only the prototype-pollution vector, not general trust of merged input — validate before you merge caller-supplied data.
99
-
100
- ## Reading
101
-
102
- ```ts
103
- const userId = userContext.get("userId"); // TStore[K] | undefined
104
- const store = userContext.getStore(); // TStore | undefined
105
- const inside = userContext.hasContext(); // boolean
106
- ```
107
-
108
- `get` is the daily-use accessor. `getStore` returns the whole record. `hasContext` distinguishes "key absent" from "no context at all" (which matters for safety checks at the framework boundary).
109
-
110
- ## Writing within a context
111
-
112
- ```ts
113
- userContext.set("role", "admin"); // sugar for update({ role: "admin" })
114
- ```
115
-
116
- Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want.
117
-
118
- ## Clearing
119
-
120
- ```ts
121
- userContext.clear();
122
- // replaces the current store with an empty object of TStore
123
- ```
124
-
125
- Rare in app code. The auto-cleanup at the end of `run()` is the normal path.
126
-
127
- ## Convenience getters via subclass
128
-
129
- Add domain-friendly getters on the subclass when a key is read a lot:
130
-
131
- ```ts
132
- class TenantContext extends Context<TenantStore> {
133
- public buildStore(payload?: Record<string, any>): TenantStore {
134
- return {
135
- tenantId: payload?.tenantId ?? "",
136
- tenantName: payload?.tenantName ?? "",
137
- config: payload?.config ?? {},
138
- };
139
- }
140
-
141
- public get tenantId() {
142
- return this.get("tenantId");
143
- }
144
-
145
- public get config() {
146
- return this.get("config");
147
- }
148
- }
149
- ```
150
-
151
- Now `tenantContext.tenantId` reads better than `tenantContext.get("tenantId")` at the call site.
152
-
153
- ## What it's NOT for
154
-
155
- - **Persistent state across requests.** AsyncLocalStorage is per-call-tree; data dies when the scope ends. Use a cache, a database, or a singleton for cross-request data.
156
- - **Thread-safe shared mutable state.** Each `run()` gets a fresh store. Two parallel `run()` calls don't see each other's updates.
157
- - **Sync code that doesn't `await` anything.** Works, but if there's no async boundary the context add-overhead is wasted — just pass the data as a parameter.
158
-
159
- ## See also
160
-
161
- - [`@warlock.js/context/orchestrate-contexts/SKILL.md`](@warlock.js/context/orchestrate-contexts/SKILL.md) — running multiple contexts together via the `contextManager` singleton.
162
-
163
- ## Things NOT to do
164
-
165
- - Don't make every cross-cutting concern a context. Build one when it has its own lifecycle (request, db transaction, trace span). For one-off data, a function argument is clearer.
166
- - Don't capture the store reference outside the scope — it's freed when `run()` ends. Read the value out before exiting the scope if you need it later.
167
- - Don't mutate the store object directly without `update`/`set` — works, but obscures intent. The methods exist to make state changes searchable.
168
- - Don't share one context instance across unrelated concerns. One typed context per domain concept reads better than one fat `globalContext`.
9
+ ---
10
+ name: define-context
11
+ description: 'Extend Context<TStore> to define an AsyncLocalStorage-backed typed context — implement buildStore, use run / enter / update / get / set / getStore / clear / hasContext. Triggers: `Context`, `Context<TStore>`, `buildStore`, `run`, `enter`, `update`, `get`, `set`, `getStore`, `clear`, `hasContext`; "share user/tenant/trace id across async calls", "AsyncLocalStorage typed wrapper", "request-scoped store without thread-through"; typical import `import { Context } from "@warlock.js/context"`. Skip: orchestrating multiple contexts — `@warlock.js/context/orchestrate-contexts/SKILL.md`; native `AsyncLocalStorage`, `cls-hooked`, `nest-context`, React Context.'
12
+ ---
13
+
14
+ # Define a context
15
+
16
+ `@warlock.js/context` is a tiny wrapper on Node.js's `AsyncLocalStorage`. You extend the abstract `Context<TStore>` class to declare what your context stores, and you get a typed get/set/run API that propagates through every async call inside the scope.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @warlock.js/context
22
+ ```
23
+
24
+ ## Shape
25
+
26
+ ```ts
27
+ import { Context } from "@warlock.js/context";
28
+
29
+ interface UserContextStore {
30
+ userId: string;
31
+ role: "admin" | "user";
32
+ tenantId: string;
33
+ }
34
+
35
+ class UserContext extends Context<UserContextStore> {
36
+ /**
37
+ * Called by `contextManager.buildStores(payload)` for each registered context.
38
+ * Override to provide initialization logic for this context's store.
39
+ */
40
+ public buildStore(payload?: Record<string, any>): UserContextStore {
41
+ return {
42
+ userId: payload?.userId ?? "",
43
+ role: payload?.role ?? "user",
44
+ tenantId: payload?.tenantId ?? "",
45
+ };
46
+ }
47
+ }
48
+
49
+ export const userContext = new UserContext();
50
+ ```
51
+
52
+ `buildStore` is the **only** abstract method. Everything else is provided by `Context<TStore>`.
53
+
54
+ ## Usage modes
55
+
56
+ ### `run()` — scoped execution
57
+
58
+ ```ts
59
+ await userContext.run(
60
+ { userId: "123", role: "admin", tenantId: "acme" },
61
+ async () => {
62
+ // Context is available throughout this async scope and any awaited calls inside it.
63
+ const userId = userContext.get("userId"); // "123"
64
+ const role = userContext.get("role"); // "admin"
65
+
66
+ await someAsyncOperation(); // context propagates through awaits
67
+ },
68
+ );
69
+ ```
70
+
71
+ Use `run()` when you have a clear scope boundary (a request handler, a job, a CLI command). The context is auto-cleaned when the callback returns.
72
+
73
+ ### `enter()` — middleware-style, no callback
74
+
75
+ ```ts
76
+ function authMiddleware(req, res, next) {
77
+ userContext.enter({
78
+ userId: req.user.id,
79
+ role: req.user.role,
80
+ tenantId: req.headers["x-tenant-id"],
81
+ });
82
+
83
+ next(); // context lives for the rest of the request
84
+ }
85
+ ```
86
+
87
+ Use `enter()` when the framework doesn't give you a callback to wrap (Express-style middleware). Under the hood it's `AsyncLocalStorage.enterWith(store)`.
88
+
89
+ ### `update()` — merge into the current context
90
+
91
+ ```ts
92
+ userContext.update({ role: "admin" });
93
+ // existing store: { userId, role, tenantId } → { userId, role: "admin", tenantId }
94
+ ```
95
+
96
+ If there's no current store, `update` creates one with the partial (cast to the full type). Use for incremental enrichment as the request flows through layers.
97
+
98
+ `update()` (and `set()`, which calls it) silently drops `__proto__` / `constructor` / `prototype` keys instead of merging them — merging with `Object.assign` would otherwise let a caller-shaped payload (e.g. `userContext.update(req.body)`) pollute `Object.prototype` for the whole process, since the context store is shared. The rest of the payload still merges normally; this closes only the prototype-pollution vector, not general trust of merged input — validate before you merge caller-supplied data.
99
+
100
+ ## Reading
101
+
102
+ ```ts
103
+ const userId = userContext.get("userId"); // TStore[K] | undefined
104
+ const store = userContext.getStore(); // TStore | undefined
105
+ const inside = userContext.hasContext(); // boolean
106
+ ```
107
+
108
+ `get` is the daily-use accessor. `getStore` returns the whole record. `hasContext` distinguishes "key absent" from "no context at all" (which matters for safety checks at the framework boundary).
109
+
110
+ ## Writing within a context
111
+
112
+ ```ts
113
+ userContext.set("role", "admin"); // sugar for update({ role: "admin" })
114
+ ```
115
+
116
+ Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want.
117
+
118
+ ## Clearing
119
+
120
+ ```ts
121
+ userContext.clear();
122
+ // replaces the current store with an empty object of TStore
123
+ ```
124
+
125
+ Rare in app code. The auto-cleanup at the end of `run()` is the normal path.
126
+
127
+ ## Convenience getters via subclass
128
+
129
+ Add domain-friendly getters on the subclass when a key is read a lot:
130
+
131
+ ```ts
132
+ class TenantContext extends Context<TenantStore> {
133
+ public buildStore(payload?: Record<string, any>): TenantStore {
134
+ return {
135
+ tenantId: payload?.tenantId ?? "",
136
+ tenantName: payload?.tenantName ?? "",
137
+ config: payload?.config ?? {},
138
+ };
139
+ }
140
+
141
+ public get tenantId() {
142
+ return this.get("tenantId");
143
+ }
144
+
145
+ public get config() {
146
+ return this.get("config");
147
+ }
148
+ }
149
+ ```
150
+
151
+ Now `tenantContext.tenantId` reads better than `tenantContext.get("tenantId")` at the call site.
152
+
153
+ ## What it's NOT for
154
+
155
+ - **Persistent state across requests.** AsyncLocalStorage is per-call-tree; data dies when the scope ends. Use a cache, a database, or a singleton for cross-request data.
156
+ - **Thread-safe shared mutable state.** Each `run()` gets a fresh store. Two parallel `run()` calls don't see each other's updates.
157
+ - **Sync code that doesn't `await` anything.** Works, but if there's no async boundary the context add-overhead is wasted — just pass the data as a parameter.
158
+
159
+ ## See also
160
+
161
+ - [`@warlock.js/context/orchestrate-contexts/SKILL.md`](@warlock.js/context/orchestrate-contexts/SKILL.md) — running multiple contexts together via the `contextManager` singleton.
162
+
163
+ ## Things NOT to do
164
+
165
+ - Don't make every cross-cutting concern a context. Build one when it has its own lifecycle (request, db transaction, trace span). For one-off data, a function argument is clearer.
166
+ - Don't capture the store reference outside the scope — it's freed when `run()` ends. Read the value out before exiting the scope if you need it later.
167
+ - Don't mutate the store object directly without `update`/`set` — works, but obscures intent. The methods exist to make state changes searchable.
168
+ - Don't share one context instance across unrelated concerns. One typed context per domain concept reads better than one fat `globalContext`.
169
169
 
170
170
 
171
171
  ## orchestrate-contexts `@warlock.js/context/orchestrate-contexts/SKILL.md`
172
172
 
173
- ---
174
- name: orchestrate-contexts
175
- description: 'Orchestrate multiple Context<TStore> instances via the contextManager singleton — register, buildStores, runAll, enterAll, clearAll. Triggers: `contextManager`, `register`, `buildStores`, `runAll`, `enterAll`, `clearAll`, `unregister`, `getContext`, `hasContext`; "run multiple contexts active for the same scope", "register contexts at boot", "avoid nested run() calls for request + database + tenant"; typical import `import { contextManager } from "@warlock.js/context"`. Skip: defining a single context class — `@warlock.js/context/define-context/SKILL.md`; native `AsyncLocalStorage` nesting, `cls-hooked` namespaces.'
176
- ---
177
-
178
- # Orchestrate contexts — `contextManager`
179
-
180
- `contextManager` is a singleton that knows about every registered `Context` and runs them all together so you don't write nested `run()` calls by hand.
181
-
182
- ## Why use it
183
-
184
- You can call `context.run(store, fn)` directly when you only have one context. With two or more, the manager handles the nesting:
185
-
186
- ```ts
187
- // ❌ Without the manager — fragile, easy to forget a layer
188
- await requestContext.run(reqStore, async () =>
189
- databaseContext.run(dbStore, async () =>
190
- tenantContext.run(tenantStore, async () => handle()),
191
- ),
192
- );
193
-
194
- // ✅ With the manager — one call, deterministic order
195
- await contextManager.runAll({ request: reqStore, database: dbStore, tenant: tenantStore }, handle);
196
- ```
197
-
198
- ## Register contexts at boot
199
-
200
- ```ts
201
- import { contextManager } from "@warlock.js/context";
202
- import { requestContext } from "./request-context";
203
- import { databaseContext } from "./database-context";
204
- import { tenantContext } from "./tenant-context";
205
-
206
- contextManager
207
- .register("request", requestContext)
208
- .register("database", databaseContext)
209
- .register("tenant", tenantContext);
210
- ```
211
-
212
- Returns the manager — chain registrations. Names must be unique; re-registering with the same name overwrites.
213
-
214
- ## Build stores + run
215
-
216
- The typical flow is two-step: build initial stores from a request-like payload, then run.
217
-
218
- ```ts
219
- app.use(async (req, res, next) => {
220
- const stores = contextManager.buildStores({
221
- request: req,
222
- response: res,
223
- tenantId: req.headers["x-tenant-id"],
224
- });
225
-
226
- await contextManager.runAll(stores, async () => {
227
- await next();
228
- });
229
- });
230
- ```
231
-
232
- `buildStores(payload)` calls each registered context's `buildStore(payload)` (the abstract method on `Context<TStore>` — see [`@warlock.js/context/define-context/SKILL.md`](@warlock.js/context/define-context/SKILL.md)) and returns `{ [contextName]: store }`.
233
-
234
- `runAll(stores, fn)` nests every context's `run()` in registration order, then invokes `fn` at the innermost layer. All contexts are active inside `fn`.
235
-
236
- ## `enterAll()` — middleware without a callback
237
-
238
- When the framework doesn't give you a callback to wrap (e.g. Express middleware where you call `next()` and return):
239
-
240
- ```ts
241
- function contextMiddleware(req, res, next) {
242
- const stores = contextManager.buildStores({ request: req, response: res });
243
- contextManager.enterAll(stores);
244
- next();
245
- }
246
- ```
247
-
248
- `enterAll` calls `enter()` on each registered context whose name has a **truthy** store value — a `name` with no key (or a falsy value like `undefined`) is skipped, leaving any already-active store for that context untouched. The entered contexts live for the rest of the request. Each can still be `clear()`-ed later. Use `runAll` when you can — `enterAll` doesn't auto-clean.
249
-
250
- ## Lookup + introspection
251
-
252
- ```ts
253
- contextManager.hasContext("tenant"); // boolean
254
- const tenant = contextManager.getContext<TenantContext>("tenant");
255
- // returns the registered instance or undefined
256
- contextManager.unregister("debug"); // remove a context
257
- contextManager.clearAll(); // clear stores on every context
258
- ```
259
-
260
- `getContext<T>` returns the registered instance cast to `T` (the generic is constrained to `Context<any>`, so pass the concrete context class — `getContext<TenantContext>("tenant")`). It's an unchecked cast: an unknown `name` returns `undefined`, and a wrong type argument won't be caught at runtime. Useful in shared utilities that want to read from a context without importing it at the call site.
261
-
262
- ## Order matters
263
-
264
- `runAll` nests in **registration order**. The first-registered context is the outermost layer. If contexts have ordering constraints (database before tenant because tenant resolves via the db), register them in that order.
265
-
266
- ```ts
267
- contextManager
268
- .register("trace", traceContext) // outermost — runs first
269
- .register("request", requestContext)
270
- .register("database", databaseContext)
271
- .register("tenant", tenantContext); // innermost — runs last, inside all others
272
- ```
273
-
274
- ## Real-world: multi-tenant request lifecycle
275
-
276
- ```ts
277
- import { Context, contextManager } from "@warlock.js/context";
278
- import { randomUUID } from "crypto";
279
-
280
- class TraceContext extends Context<{ traceId: string; startTime: number }> {
281
- public buildStore(): { traceId: string; startTime: number } {
282
- return { traceId: randomUUID(), startTime: Date.now() };
283
- }
284
-
285
- public get traceId() {
286
- return this.get("traceId");
287
- }
288
- }
289
-
290
- class RequestContext extends Context<{ request: any; response: any }> {
291
- public buildStore(payload?: any) {
292
- return { request: payload?.request, response: payload?.response };
293
- }
294
- }
295
-
296
- class TenantContext extends Context<{ tenantId: string }> {
297
- public buildStore(payload?: any) {
298
- return { tenantId: payload?.tenantId ?? "" };
299
- }
300
- }
301
-
302
- export const traceContext = new TraceContext();
303
- export const requestContext = new RequestContext();
304
- export const tenantContext = new TenantContext();
305
-
306
- contextManager
307
- .register("trace", traceContext)
308
- .register("request", requestContext)
309
- .register("tenant", tenantContext);
310
-
311
- // In your HTTP layer:
312
- async function handleRequest(req: any, res: any) {
313
- const stores = contextManager.buildStores({
314
- request: req,
315
- response: res,
316
- tenantId: req.headers["x-tenant-id"],
317
- });
318
-
319
- return contextManager.runAll(stores, async () => {
320
- // All three contexts active here:
321
- console.log(`Trace ${traceContext.get("traceId")} — tenant ${tenantContext.get("tenantId")}`);
322
-
323
- await routeAndDispatch(req, res);
324
- });
325
- }
326
- ```
327
-
328
- ## Things NOT to do
329
-
330
- - Don't register the same context under two names — every context is its own singleton and shares its store across registrations, but multiple names confuse `buildStores` (the payload is split per name).
331
- - Don't `runAll` an empty stores map — every key without a registered context is silently ignored, and missing contexts get `{}` as their store. Better to construct the stores explicitly and fail loudly when a key is missing.
332
- - Don't use the manager when only one context applies. `context.run(store, fn)` is shorter and has the same semantics.
333
- - Don't expect `enterAll()` to auto-clean. It's a one-way setup — pair with `clearAll()` at the end of the request, or just use `runAll` when you can.
173
+ ---
174
+ name: orchestrate-contexts
175
+ description: 'Orchestrate multiple Context<TStore> instances via the contextManager singleton — register, buildStores, runAll, enterAll, clearAll. Triggers: `contextManager`, `register`, `buildStores`, `runAll`, `enterAll`, `clearAll`, `unregister`, `getContext`, `hasContext`; "run multiple contexts active for the same scope", "register contexts at boot", "avoid nested run() calls for request + database + tenant"; typical import `import { contextManager } from "@warlock.js/context"`. Skip: defining a single context class — `@warlock.js/context/define-context/SKILL.md`; native `AsyncLocalStorage` nesting, `cls-hooked` namespaces.'
176
+ ---
177
+
178
+ # Orchestrate contexts — `contextManager`
179
+
180
+ `contextManager` is a singleton that knows about every registered `Context` and runs them all together so you don't write nested `run()` calls by hand.
181
+
182
+ ## Why use it
183
+
184
+ You can call `context.run(store, fn)` directly when you only have one context. With two or more, the manager handles the nesting:
185
+
186
+ ```ts
187
+ // ❌ Without the manager — fragile, easy to forget a layer
188
+ await requestContext.run(reqStore, async () =>
189
+ databaseContext.run(dbStore, async () =>
190
+ tenantContext.run(tenantStore, async () => handle()),
191
+ ),
192
+ );
193
+
194
+ // ✅ With the manager — one call, deterministic order
195
+ await contextManager.runAll({ request: reqStore, database: dbStore, tenant: tenantStore }, handle);
196
+ ```
197
+
198
+ ## Register contexts at boot
199
+
200
+ ```ts
201
+ import { contextManager } from "@warlock.js/context";
202
+ import { requestContext } from "./request-context";
203
+ import { databaseContext } from "./database-context";
204
+ import { tenantContext } from "./tenant-context";
205
+
206
+ contextManager
207
+ .register("request", requestContext)
208
+ .register("database", databaseContext)
209
+ .register("tenant", tenantContext);
210
+ ```
211
+
212
+ Returns the manager — chain registrations. Names must be unique; re-registering with the same name overwrites.
213
+
214
+ ## Build stores + run
215
+
216
+ The typical flow is two-step: build initial stores from a request-like payload, then run.
217
+
218
+ ```ts
219
+ app.use(async (req, res, next) => {
220
+ const stores = contextManager.buildStores({
221
+ request: req,
222
+ response: res,
223
+ tenantId: req.headers["x-tenant-id"],
224
+ });
225
+
226
+ await contextManager.runAll(stores, async () => {
227
+ await next();
228
+ });
229
+ });
230
+ ```
231
+
232
+ `buildStores(payload)` calls each registered context's `buildStore(payload)` (the abstract method on `Context<TStore>` — see [`@warlock.js/context/define-context/SKILL.md`](@warlock.js/context/define-context/SKILL.md)) and returns `{ [contextName]: store }`.
233
+
234
+ `runAll(stores, fn)` nests every context's `run()` in registration order, then invokes `fn` at the innermost layer. All contexts are active inside `fn`.
235
+
236
+ ## `enterAll()` — middleware without a callback
237
+
238
+ When the framework doesn't give you a callback to wrap (e.g. Express middleware where you call `next()` and return):
239
+
240
+ ```ts
241
+ function contextMiddleware(req, res, next) {
242
+ const stores = contextManager.buildStores({ request: req, response: res });
243
+ contextManager.enterAll(stores);
244
+ next();
245
+ }
246
+ ```
247
+
248
+ `enterAll` calls `enter()` on each registered context whose name has a **truthy** store value — a `name` with no key (or a falsy value like `undefined`) is skipped, leaving any already-active store for that context untouched. The entered contexts live for the rest of the request. Each can still be `clear()`-ed later. Use `runAll` when you can — `enterAll` doesn't auto-clean.
249
+
250
+ ## Lookup + introspection
251
+
252
+ ```ts
253
+ contextManager.hasContext("tenant"); // boolean
254
+ const tenant = contextManager.getContext<TenantContext>("tenant");
255
+ // returns the registered instance or undefined
256
+ contextManager.unregister("debug"); // remove a context
257
+ contextManager.clearAll(); // clear stores on every context
258
+ ```
259
+
260
+ `getContext<T>` returns the registered instance cast to `T` (the generic is constrained to `Context<any>`, so pass the concrete context class — `getContext<TenantContext>("tenant")`). It's an unchecked cast: an unknown `name` returns `undefined`, and a wrong type argument won't be caught at runtime. Useful in shared utilities that want to read from a context without importing it at the call site.
261
+
262
+ ## Order matters
263
+
264
+ `runAll` nests in **registration order**. The first-registered context is the outermost layer. If contexts have ordering constraints (database before tenant because tenant resolves via the db), register them in that order.
265
+
266
+ ```ts
267
+ contextManager
268
+ .register("trace", traceContext) // outermost — runs first
269
+ .register("request", requestContext)
270
+ .register("database", databaseContext)
271
+ .register("tenant", tenantContext); // innermost — runs last, inside all others
272
+ ```
273
+
274
+ ## Real-world: multi-tenant request lifecycle
275
+
276
+ ```ts
277
+ import { Context, contextManager } from "@warlock.js/context";
278
+ import { randomUUID } from "crypto";
279
+
280
+ class TraceContext extends Context<{ traceId: string; startTime: number }> {
281
+ public buildStore(): { traceId: string; startTime: number } {
282
+ return { traceId: randomUUID(), startTime: Date.now() };
283
+ }
284
+
285
+ public get traceId() {
286
+ return this.get("traceId");
287
+ }
288
+ }
289
+
290
+ class RequestContext extends Context<{ request: any; response: any }> {
291
+ public buildStore(payload?: any) {
292
+ return { request: payload?.request, response: payload?.response };
293
+ }
294
+ }
295
+
296
+ class TenantContext extends Context<{ tenantId: string }> {
297
+ public buildStore(payload?: any) {
298
+ return { tenantId: payload?.tenantId ?? "" };
299
+ }
300
+ }
301
+
302
+ export const traceContext = new TraceContext();
303
+ export const requestContext = new RequestContext();
304
+ export const tenantContext = new TenantContext();
305
+
306
+ contextManager
307
+ .register("trace", traceContext)
308
+ .register("request", requestContext)
309
+ .register("tenant", tenantContext);
310
+
311
+ // In your HTTP layer:
312
+ async function handleRequest(req: any, res: any) {
313
+ const stores = contextManager.buildStores({
314
+ request: req,
315
+ response: res,
316
+ tenantId: req.headers["x-tenant-id"],
317
+ });
318
+
319
+ return contextManager.runAll(stores, async () => {
320
+ // All three contexts active here:
321
+ console.log(`Trace ${traceContext.get("traceId")} — tenant ${tenantContext.get("tenantId")}`);
322
+
323
+ await routeAndDispatch(req, res);
324
+ });
325
+ }
326
+ ```
327
+
328
+ ## Things NOT to do
329
+
330
+ - Don't register the same context under two names — every context is its own singleton and shares its store across registrations, but multiple names confuse `buildStores` (the payload is split per name).
331
+ - Don't `runAll` an empty stores map — every key without a registered context is silently ignored, and missing contexts get `{}` as their store. Better to construct the stores explicitly and fail loudly when a key is missing.
332
+ - Don't use the manager when only one context applies. `context.run(store, fn)` is shorter and has the same semantics.
333
+ - Don't expect `enterAll()` to auto-clean. It's a one-way setup — pair with `clearAll()` at the end of the request, or just use `runAll` when you can.
334
334
 
335
335
 
336
336
  ## overview `@warlock.js/context/overview/SKILL.md`