@warlock.js/context 4.0.174 → 4.1.2
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/cjs/index.cjs +239 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/base-context.d.mts +99 -0
- package/esm/base-context.d.mts.map +1 -0
- package/esm/base-context.mjs +110 -0
- package/esm/base-context.mjs.map +1 -0
- package/esm/context-manager.d.mts +106 -0
- package/esm/context-manager.d.mts.map +1 -0
- package/esm/context-manager.mjs +128 -0
- package/esm/context-manager.mjs.map +1 -0
- package/esm/index.d.mts +3 -0
- package/esm/index.mjs +4 -0
- package/llms-full.txt +404 -0
- package/llms.txt +11 -0
- package/package.json +42 -30
- package/skills/define-context/SKILL.md +158 -0
- package/skills/orchestrate-contexts/SKILL.md +161 -0
- package/skills/overview/SKILL.md +67 -0
- package/cjs/base-context.d.ts +0 -96
- package/cjs/base-context.d.ts.map +0 -1
- package/cjs/base-context.js +0 -105
- package/cjs/base-context.js.map +0 -1
- package/cjs/context-manager.d.ts +0 -102
- package/cjs/context-manager.d.ts.map +0 -1
- package/cjs/context-manager.js +0 -132
- package/cjs/context-manager.js.map +0 -1
- package/cjs/index.d.ts +0 -3
- package/cjs/index.d.ts.map +0 -1
- package/cjs/index.js +0 -1
- package/cjs/index.js.map +0 -1
- package/esm/base-context.d.ts +0 -96
- package/esm/base-context.d.ts.map +0 -1
- package/esm/base-context.js +0 -105
- package/esm/base-context.js.map +0 -1
- package/esm/context-manager.d.ts +0 -102
- package/esm/context-manager.d.ts.map +0 -1
- package/esm/context-manager.js +0 -132
- package/esm/context-manager.js.map +0 -1
- package/esm/index.d.ts +0 -3
- package/esm/index.d.ts.map +0 -1
- package/esm/index.js +0 -1
- package/esm/index.js.map +0 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//#region ../../@warlock.js/context/src/context-manager.ts
|
|
2
|
+
/**
|
|
3
|
+
* Context Manager - Orchestrates multiple contexts together
|
|
4
|
+
*
|
|
5
|
+
* Allows running multiple AsyncLocalStorage contexts in a single operation,
|
|
6
|
+
* making it easy to link request, storage, database, and other contexts.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Register contexts
|
|
11
|
+
* contextManager
|
|
12
|
+
* .register('request', requestContext)
|
|
13
|
+
* .register('storage', storageDriverContext)
|
|
14
|
+
* .register('database', databaseDataSourceContext);
|
|
15
|
+
*
|
|
16
|
+
* // Run all contexts together
|
|
17
|
+
* await contextManager.runAll({
|
|
18
|
+
* request: { request, response, user },
|
|
19
|
+
* storage: { driver, metadata: { tenantId: '123' } },
|
|
20
|
+
* database: { dataSource: 'primary' },
|
|
21
|
+
* }, async () => {
|
|
22
|
+
* // All contexts active!
|
|
23
|
+
* await handleRequest();
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
var ContextManager = class {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.contexts = /* @__PURE__ */ new Map();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Register a context
|
|
33
|
+
*
|
|
34
|
+
* @param name - Unique context name
|
|
35
|
+
* @param context - Context instance
|
|
36
|
+
* @returns This instance for chaining
|
|
37
|
+
*/
|
|
38
|
+
register(name, context) {
|
|
39
|
+
this.contexts.set(name, context);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Run all registered contexts together
|
|
44
|
+
*
|
|
45
|
+
* Nests all context.run() calls, ensuring all contexts are active
|
|
46
|
+
* for the duration of the callback.
|
|
47
|
+
*
|
|
48
|
+
* @param stores - Context stores keyed by context name
|
|
49
|
+
* @param callback - Async function to execute
|
|
50
|
+
* @returns Result of the callback
|
|
51
|
+
*/
|
|
52
|
+
async runAll(stores, callback) {
|
|
53
|
+
return Array.from(this.contexts.entries()).reduceRight((next, [name, context]) => {
|
|
54
|
+
return () => context.run(stores[name] || {}, next);
|
|
55
|
+
}, callback)();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Enter all contexts at once (for middleware)
|
|
59
|
+
*
|
|
60
|
+
* @param stores - Context stores keyed by context name
|
|
61
|
+
*/
|
|
62
|
+
enterAll(stores) {
|
|
63
|
+
for (const [name, context] of this.contexts.entries()) if (stores[name]) context.enter(stores[name]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Clear all contexts
|
|
67
|
+
*/
|
|
68
|
+
clearAll() {
|
|
69
|
+
for (const context of this.contexts.values()) context.clear();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get a specific registered context
|
|
73
|
+
*
|
|
74
|
+
* @param name - Context name
|
|
75
|
+
* @returns Context instance or undefined
|
|
76
|
+
*/
|
|
77
|
+
getContext(name) {
|
|
78
|
+
return this.contexts.get(name);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if a context is registered
|
|
82
|
+
*
|
|
83
|
+
* @param name - Context name
|
|
84
|
+
* @returns True if context is registered
|
|
85
|
+
*/
|
|
86
|
+
hasContext(name) {
|
|
87
|
+
return this.contexts.has(name);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build all context stores by calling each context's buildStore() method
|
|
91
|
+
*
|
|
92
|
+
* This is the immutable pattern - returns a new record of stores.
|
|
93
|
+
* Each context defines its own initialization logic.
|
|
94
|
+
*
|
|
95
|
+
* @param payload - Payload passed to each buildStore() (e.g., { request, response })
|
|
96
|
+
* @returns Record of context name -> store data
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const httpContextStore = contextManager.buildStores({ request, response });
|
|
101
|
+
* await contextManager.runAll(httpContextStore, async () => { ... });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
buildStores(payload) {
|
|
105
|
+
const stores = {};
|
|
106
|
+
for (const [name, context] of this.contexts.entries()) stores[name] = context.buildStore(payload) ?? {};
|
|
107
|
+
return stores;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Unregister a context
|
|
111
|
+
*
|
|
112
|
+
* @param name - Context name to remove
|
|
113
|
+
* @returns True if context was removed
|
|
114
|
+
*/
|
|
115
|
+
unregister(name) {
|
|
116
|
+
return this.contexts.delete(name);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Global context manager instance
|
|
121
|
+
*
|
|
122
|
+
* Use this singleton to register and manage all framework contexts.
|
|
123
|
+
*/
|
|
124
|
+
const contextManager = new ContextManager();
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
export { ContextManager, contextManager };
|
|
128
|
+
//# sourceMappingURL=context-manager.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-manager.mjs","names":[],"sources":["../../../../../@warlock.js/context/src/context-manager.ts"],"sourcesContent":["import type { Context } from \"./base-context\";\n\n/**\n * Context Manager - Orchestrates multiple contexts together\n *\n * Allows running multiple AsyncLocalStorage contexts in a single operation,\n * making it easy to link request, storage, database, and other contexts.\n *\n * @example\n * ```typescript\n * // Register contexts\n * contextManager\n * .register('request', requestContext)\n * .register('storage', storageDriverContext)\n * .register('database', databaseDataSourceContext);\n *\n * // Run all contexts together\n * await contextManager.runAll({\n * request: { request, response, user },\n * storage: { driver, metadata: { tenantId: '123' } },\n * database: { dataSource: 'primary' },\n * }, async () => {\n * // All contexts active!\n * await handleRequest();\n * });\n * ```\n */\nexport class ContextManager {\n private contexts = new Map<string, Context<any>>();\n\n /**\n * Register a context\n *\n * @param name - Unique context name\n * @param context - Context instance\n * @returns This instance for chaining\n */\n public register(name: string, context: Context<any>): this {\n this.contexts.set(name, context);\n return this;\n }\n\n /**\n * Run all registered contexts together\n *\n * Nests all context.run() calls, ensuring all contexts are active\n * for the duration of the callback.\n *\n * @param stores - Context stores keyed by context name\n * @param callback - Async function to execute\n * @returns Result of the callback\n */\n public async runAll<T>(stores: Record<string, any>, callback: () => Promise<T>): Promise<T> {\n const entries = Array.from(this.contexts.entries());\n\n // Build nested context runners\n const runner = entries.reduceRight((next, [name, context]) => {\n return () => context.run(stores[name] || {}, next);\n }, callback);\n\n return runner();\n }\n\n /**\n * Enter all contexts at once (for middleware)\n *\n * @param stores - Context stores keyed by context name\n */\n public enterAll(stores: Record<string, any>): void {\n for (const [name, context] of this.contexts.entries()) {\n if (stores[name]) {\n context.enter(stores[name]);\n }\n }\n }\n\n /**\n * Clear all contexts\n */\n public clearAll(): void {\n for (const context of this.contexts.values()) {\n context.clear();\n }\n }\n\n /**\n * Get a specific registered context\n *\n * @param name - Context name\n * @returns Context instance or undefined\n */\n public getContext<T extends Context<any>>(name: string): T | undefined {\n return this.contexts.get(name) as T | undefined;\n }\n\n /**\n * Check if a context is registered\n *\n * @param name - Context name\n * @returns True if context is registered\n */\n public hasContext(name: string): boolean {\n return this.contexts.has(name);\n }\n\n /**\n * Build all context stores by calling each context's buildStore() method\n *\n * This is the immutable pattern - returns a new record of stores.\n * Each context defines its own initialization logic.\n *\n * @param payload - Payload passed to each buildStore() (e.g., { request, response })\n * @returns Record of context name -> store data\n *\n * @example\n * ```typescript\n * const httpContextStore = contextManager.buildStores({ request, response });\n * await contextManager.runAll(httpContextStore, async () => { ... });\n * ```\n */\n public buildStores(payload?: Record<string, any>): Record<string, any> {\n const stores: Record<string, any> = {};\n\n for (const [name, context] of this.contexts.entries()) {\n stores[name] = context.buildStore(payload) ?? {};\n }\n\n return stores;\n }\n\n /**\n * Unregister a context\n *\n * @param name - Context name to remove\n * @returns True if context was removed\n */\n public unregister(name: string): boolean {\n return this.contexts.delete(name);\n }\n}\n\n/**\n * Global context manager instance\n *\n * Use this singleton to register and manage all framework contexts.\n */\nexport const contextManager = new ContextManager();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,IAAa,iBAAb,MAA4B;;kCACP,IAAI,IAA0B;;;;;;;;;CASjD,AAAO,SAAS,MAAc,SAA6B;EACzD,KAAK,SAAS,IAAI,MAAM,OAAO;EAC/B,OAAO;CACT;;;;;;;;;;;CAYA,MAAa,OAAU,QAA6B,UAAwC;EAQ1F,OAPgB,MAAM,KAAK,KAAK,SAAS,QAAQ,CAG5B,EAAE,aAAa,MAAM,CAAC,MAAM,aAAa;GAC5D,aAAa,QAAQ,IAAI,OAAO,SAAS,CAAC,GAAG,IAAI;EACnD,GAAG,QAES,EAAE;CAChB;;;;;;CAOA,AAAO,SAAS,QAAmC;EACjD,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SAAS,QAAQ,GAClD,IAAI,OAAO,OACT,QAAQ,MAAM,OAAO,KAAK;CAGhC;;;;CAKA,AAAO,WAAiB;EACtB,KAAK,MAAM,WAAW,KAAK,SAAS,OAAO,GACzC,QAAQ,MAAM;CAElB;;;;;;;CAQA,AAAO,WAAmC,MAA6B;EACrE,OAAO,KAAK,SAAS,IAAI,IAAI;CAC/B;;;;;;;CAQA,AAAO,WAAW,MAAuB;EACvC,OAAO,KAAK,SAAS,IAAI,IAAI;CAC/B;;;;;;;;;;;;;;;;CAiBA,AAAO,YAAY,SAAoD;EACrE,MAAM,SAA8B,CAAC;EAErC,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SAAS,QAAQ,GAClD,OAAO,QAAQ,QAAQ,WAAW,OAAO,KAAK,CAAC;EAGjD,OAAO;CACT;;;;;;;CAQA,AAAO,WAAW,MAAuB;EACvC,OAAO,KAAK,SAAS,OAAO,IAAI;CAClC;AACF;;;;;;AAOA,MAAa,iBAAiB,IAAI,eAAe"}
|
package/esm/index.d.mts
ADDED
package/esm/index.mjs
ADDED
package/llms-full.txt
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Warlock Context — full skills
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/context`
|
|
4
|
+
|
|
5
|
+
> Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/context/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
|
|
6
|
+
|
|
7
|
+
## define-context `@warlock.js/context/define-context/SKILL.md`
|
|
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
|
+
yarn add @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
|
+
## Reading
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const userId = userContext.get("userId"); // TStore[K] | undefined
|
|
102
|
+
const store = userContext.getStore(); // TStore | undefined
|
|
103
|
+
const inside = userContext.hasContext(); // boolean
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`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).
|
|
107
|
+
|
|
108
|
+
## Writing within a context
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
userContext.set("role", "admin"); // sugar for update({ role: "admin" })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want.
|
|
115
|
+
|
|
116
|
+
## Clearing
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
userContext.clear();
|
|
120
|
+
// replaces the current store with an empty object of TStore
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Rare in app code. The auto-cleanup at the end of `run()` is the normal path.
|
|
124
|
+
|
|
125
|
+
## Convenience getters via subclass
|
|
126
|
+
|
|
127
|
+
Add domain-friendly getters on the subclass when a key is read a lot:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
class TenantContext extends Context<TenantStore> {
|
|
131
|
+
public buildStore(payload?: Record<string, any>): TenantStore {
|
|
132
|
+
return {
|
|
133
|
+
tenantId: payload?.tenantId ?? "",
|
|
134
|
+
tenantName: payload?.tenantName ?? "",
|
|
135
|
+
config: payload?.config ?? {},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public get tenantId() {
|
|
140
|
+
return this.get("tenantId");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public get config() {
|
|
144
|
+
return this.get("config");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Now `tenantContext.tenantId` reads better than `tenantContext.get("tenantId")` at the call site.
|
|
150
|
+
|
|
151
|
+
## What it's NOT for
|
|
152
|
+
|
|
153
|
+
- **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.
|
|
154
|
+
- **Thread-safe shared mutable state.** Each `run()` gets a fresh store. Two parallel `run()` calls don't see each other's updates.
|
|
155
|
+
- **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.
|
|
156
|
+
|
|
157
|
+
## See also
|
|
158
|
+
|
|
159
|
+
- [`@warlock.js/context/orchestrate-contexts/SKILL.md`](@warlock.js/context/orchestrate-contexts/SKILL.md) — running multiple contexts together via the `contextManager` singleton.
|
|
160
|
+
|
|
161
|
+
## Things NOT to do
|
|
162
|
+
|
|
163
|
+
- 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.
|
|
164
|
+
- 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.
|
|
165
|
+
- Don't mutate the store object directly without `update`/`set` — works, but obscures intent. The methods exist to make state changes searchable.
|
|
166
|
+
- Don't share one context instance across unrelated concerns. One typed context per domain concept reads better than one fat `globalContext`.
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
## orchestrate-contexts `@warlock.js/context/orchestrate-contexts/SKILL.md`
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
name: orchestrate-contexts
|
|
173
|
+
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.'
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
# Orchestrate contexts — `contextManager`
|
|
177
|
+
|
|
178
|
+
`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.
|
|
179
|
+
|
|
180
|
+
## Why use it
|
|
181
|
+
|
|
182
|
+
You can call `context.run(store, fn)` directly when you only have one context. With two or more, the manager handles the nesting:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// ❌ Without the manager — fragile, easy to forget a layer
|
|
186
|
+
await requestContext.run(reqStore, async () =>
|
|
187
|
+
databaseContext.run(dbStore, async () =>
|
|
188
|
+
tenantContext.run(tenantStore, async () => handle()),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ✅ With the manager — one call, deterministic order
|
|
193
|
+
await contextManager.runAll({ request: reqStore, database: dbStore, tenant: tenantStore }, handle);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Register contexts at boot
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { contextManager } from "@warlock.js/context";
|
|
200
|
+
import { requestContext } from "./request-context";
|
|
201
|
+
import { databaseContext } from "./database-context";
|
|
202
|
+
import { tenantContext } from "./tenant-context";
|
|
203
|
+
|
|
204
|
+
contextManager
|
|
205
|
+
.register("request", requestContext)
|
|
206
|
+
.register("database", databaseContext)
|
|
207
|
+
.register("tenant", tenantContext);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Returns the manager — chain registrations. Names must be unique; re-registering with the same name overwrites.
|
|
211
|
+
|
|
212
|
+
## Build stores + run
|
|
213
|
+
|
|
214
|
+
The typical flow is two-step: build initial stores from a request-like payload, then run.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
app.use(async (req, res, next) => {
|
|
218
|
+
const stores = contextManager.buildStores({
|
|
219
|
+
request: req,
|
|
220
|
+
response: res,
|
|
221
|
+
tenantId: req.headers["x-tenant-id"],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await contextManager.runAll(stores, async () => {
|
|
225
|
+
await next();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`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 }`.
|
|
231
|
+
|
|
232
|
+
`runAll(stores, fn)` nests every context's `run()` in registration order, then invokes `fn` at the innermost layer. All contexts are active inside `fn`.
|
|
233
|
+
|
|
234
|
+
## `enterAll()` — middleware without a callback
|
|
235
|
+
|
|
236
|
+
When the framework doesn't give you a callback to wrap (e.g. Express middleware where you call `next()` and return):
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
function contextMiddleware(req, res, next) {
|
|
240
|
+
const stores = contextManager.buildStores({ request: req, response: res });
|
|
241
|
+
contextManager.enterAll(stores);
|
|
242
|
+
next();
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`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.
|
|
247
|
+
|
|
248
|
+
## Lookup + introspection
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
contextManager.hasContext("tenant"); // boolean
|
|
252
|
+
const tenant = contextManager.getContext<TenantContext>("tenant");
|
|
253
|
+
// returns the registered instance or undefined
|
|
254
|
+
contextManager.unregister("debug"); // remove a context
|
|
255
|
+
contextManager.clearAll(); // clear stores on every context
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`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.
|
|
259
|
+
|
|
260
|
+
## Order matters
|
|
261
|
+
|
|
262
|
+
`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.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
contextManager
|
|
266
|
+
.register("trace", traceContext) // outermost — runs first
|
|
267
|
+
.register("request", requestContext)
|
|
268
|
+
.register("database", databaseContext)
|
|
269
|
+
.register("tenant", tenantContext); // innermost — runs last, inside all others
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Real-world: multi-tenant request lifecycle
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { Context, contextManager } from "@warlock.js/context";
|
|
276
|
+
import { randomUUID } from "crypto";
|
|
277
|
+
|
|
278
|
+
class TraceContext extends Context<{ traceId: string; startTime: number }> {
|
|
279
|
+
public buildStore(): { traceId: string; startTime: number } {
|
|
280
|
+
return { traceId: randomUUID(), startTime: Date.now() };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public get traceId() {
|
|
284
|
+
return this.get("traceId");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
class RequestContext extends Context<{ request: any; response: any }> {
|
|
289
|
+
public buildStore(payload?: any) {
|
|
290
|
+
return { request: payload?.request, response: payload?.response };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class TenantContext extends Context<{ tenantId: string }> {
|
|
295
|
+
public buildStore(payload?: any) {
|
|
296
|
+
return { tenantId: payload?.tenantId ?? "" };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const traceContext = new TraceContext();
|
|
301
|
+
export const requestContext = new RequestContext();
|
|
302
|
+
export const tenantContext = new TenantContext();
|
|
303
|
+
|
|
304
|
+
contextManager
|
|
305
|
+
.register("trace", traceContext)
|
|
306
|
+
.register("request", requestContext)
|
|
307
|
+
.register("tenant", tenantContext);
|
|
308
|
+
|
|
309
|
+
// In your HTTP layer:
|
|
310
|
+
async function handleRequest(req: any, res: any) {
|
|
311
|
+
const stores = contextManager.buildStores({
|
|
312
|
+
request: req,
|
|
313
|
+
response: res,
|
|
314
|
+
tenantId: req.headers["x-tenant-id"],
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return contextManager.runAll(stores, async () => {
|
|
318
|
+
// All three contexts active here:
|
|
319
|
+
console.log(`Trace ${traceContext.get("traceId")} — tenant ${tenantContext.get("tenantId")}`);
|
|
320
|
+
|
|
321
|
+
await routeAndDispatch(req, res);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Things NOT to do
|
|
327
|
+
|
|
328
|
+
- 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).
|
|
329
|
+
- 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.
|
|
330
|
+
- Don't use the manager when only one context applies. `context.run(store, fn)` is shorter and has the same semantics.
|
|
331
|
+
- 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.
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
## overview `@warlock.js/context/overview/SKILL.md`
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
name: overview
|
|
338
|
+
description: 'Front-door orientation for `@warlock.js/context` — typed AsyncLocalStorage wrappers for sharing data (user, tenant, trace, request) across async calls without thread-through. Extend `Context<TStore>` for a single context; use `contextManager` to orchestrate several at once. TRIGGER when: code imports anything from `@warlock.js/context`; user asks "what does @warlock.js/context do", "AsyncLocalStorage but typed", "share user/tenant across async without thread-through", "compare context vs cls-hooked / nest-context"; package.json adds `@warlock.js/context`. Skip: specific task already known — load the matching task skill directly (`@warlock.js/context/define-context/SKILL.md`, `@warlock.js/context/orchestrate-contexts/SKILL.md`); plain `AsyncLocalStorage` usage with no `Context<>` wrapper; React Context (this package is server-side / Node-only).'
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
# `@warlock.js/context` — overview
|
|
342
|
+
|
|
343
|
+
Two-file package: a typed wrapper over Node's `AsyncLocalStorage` and a singleton that runs several of them together. That's it. The entire surface fits in two skills below — most callers only need the first one.
|
|
344
|
+
|
|
345
|
+
## When to reach for it
|
|
346
|
+
|
|
347
|
+
- You have request-scoped data (user, tenant, trace id, db transaction) that you'd otherwise thread through every function as a parameter. One `userContext.get("userId")` anywhere down the call tree replaces five layers of plumbing.
|
|
348
|
+
- You're inside a `@warlock.js/*` project and want consistent context handling across modules. The framework already uses this package internally for request + user + tenant contexts.
|
|
349
|
+
- You'd reach for `cls-hooked`, `nest-context`, or a bare `AsyncLocalStorage<T>` and want a typed wrapper with `run` / `enter` / `update` / `get` / `set` semantics out of the box.
|
|
350
|
+
|
|
351
|
+
Skip if your call chain is a single function with no `await` boundaries — just pass the data as a parameter. Skip if you need cross-request shared state — that's a cache or database, not a context.
|
|
352
|
+
|
|
353
|
+
## What it is in one sentence
|
|
354
|
+
|
|
355
|
+
Each `Context<TStore>` subclass declares a typed store shape and what payload builds it; the framework uses Node's `AsyncLocalStorage` under the hood so the store propagates through every `await` inside the scope and disappears when the scope ends.
|
|
356
|
+
|
|
357
|
+
## Skills index
|
|
358
|
+
|
|
359
|
+
Two task skills cover everything. The first one is the daily-use one; the second is for when you have multiple contexts active at the same time.
|
|
360
|
+
|
|
361
|
+
### [`define-context/`](../define-context/SKILL.md)
|
|
362
|
+
|
|
363
|
+
Extend `Context<TStore>` to declare what your context stores and how it's
|
|
364
|
+
built. Implement `buildStore(payload?)`; use `run` / `enter` / `update` /
|
|
365
|
+
`get` / `set` / `getStore` / `clear` / `hasContext` to interact with the
|
|
366
|
+
store from anywhere in the async scope.
|
|
367
|
+
|
|
368
|
+
Load when sharing data (user, tenant, trace id) across async calls
|
|
369
|
+
without threading it through every function — i.e. ~90% of the time you
|
|
370
|
+
use this package.
|
|
371
|
+
|
|
372
|
+
### [`orchestrate-contexts/`](../orchestrate-contexts/SKILL.md)
|
|
373
|
+
|
|
374
|
+
Register multiple `Context<TStore>` instances on the `contextManager`
|
|
375
|
+
singleton and run them all together with a single call. Covers
|
|
376
|
+
`register` / `unregister`, `buildStores`, `runAll` / `enterAll`,
|
|
377
|
+
`clearAll`, `getContext` / `hasContext`.
|
|
378
|
+
|
|
379
|
+
Load when several contexts apply to the same scope (request + database
|
|
380
|
+
+ trace + tenant) and you'd otherwise nest `run()` calls by hand.
|
|
381
|
+
|
|
382
|
+
## Two operating modes — when to pick which
|
|
383
|
+
|
|
384
|
+
| Situation | Reach for |
|
|
385
|
+
| --- | --- |
|
|
386
|
+
| Framework gives you a callback to wrap | `context.run(store, callback)` |
|
|
387
|
+
| Framework expects middleware that returns synchronously (Express, etc.) | `context.enter(store)` |
|
|
388
|
+
| You have 2+ contexts active for the same request | `contextManager.runAll(stores, callback)` |
|
|
389
|
+
| Middleware-style with 2+ contexts | `contextManager.enterAll(stores)` then `clearAll()` at end |
|
|
390
|
+
| You only have one context and the framework cooperates | Single `context.run(...)`. Skip the manager. |
|
|
391
|
+
|
|
392
|
+
## What this package deliberately doesn't do
|
|
393
|
+
|
|
394
|
+
- **Persist data across requests.** AsyncLocalStorage dies with the scope. For cross-request state, reach for `@warlock.js/cache` or a database.
|
|
395
|
+
- **Cross-thread context.** Worker threads don't share AsyncLocalStorage with the main thread. Serialize the data you need across the boundary and re-enter on the other side.
|
|
396
|
+
- **Browser context.** Server-side only. The browser equivalent is React Context or a state library.
|
|
397
|
+
- **Implicit context for non-async code.** If your call stack has no `await`, the storage works but there's no propagation benefit — just pass the value as a parameter.
|
|
398
|
+
|
|
399
|
+
## See also
|
|
400
|
+
|
|
401
|
+
- [`@warlock.js/core/overview/SKILL.md`](@warlock.js/core/overview/SKILL.md) — the parent framework; `context` is one of its foundation packages.
|
|
402
|
+
- `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes the front-door skill in `.claude/skills/warlock-js-context-overview/`.
|
|
403
|
+
|
|
404
|
+
|
package/llms.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Warlock Context
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/context`
|
|
4
|
+
|
|
5
|
+
> A simple and unified way to share context using AsyncLocalStorage for the Warlock.js framework
|
|
6
|
+
|
|
7
|
+
## Skills
|
|
8
|
+
|
|
9
|
+
- [define-context](@warlock.js/context/define-context/SKILL.md): 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.
|
|
10
|
+
- [orchestrate-contexts](@warlock.js/context/orchestrate-contexts/SKILL.md): 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.
|
|
11
|
+
- [overview](@warlock.js/context/overview/SKILL.md): Front-door orientation for `@warlock.js/context` — typed AsyncLocalStorage wrappers for sharing data (user, tenant, trace, request) across async calls without thread-through. Extend `Context<TStore>` for a single context; use `contextManager` to orchestrate several at once. TRIGGER when: code imports anything from `@warlock.js/context`; user asks "what does @warlock.js/context do", "AsyncLocalStorage but typed", "share user/tenant across async without thread-through", "compare context vs cls-hooked / nest-context"; package.json adds `@warlock.js/context`. Skip: specific task already known — load the matching task skill directly (`@warlock.js/context/define-context/SKILL.md`, `@warlock.js/context/orchestrate-contexts/SKILL.md`); plain `AsyncLocalStorage` usage with no `Context<>` wrapper; React Context (this package is server-side / Node-only).
|
package/package.json
CHANGED
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
"name": "@warlock.js/context",
|
|
3
|
+
"description": "A simple and unified way to share context using AsyncLocalStorage for the Warlock.js framework",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"warlock",
|
|
6
|
+
"warlockjs",
|
|
7
|
+
"context",
|
|
8
|
+
"async-local-storage",
|
|
9
|
+
"asynclocalstorage",
|
|
10
|
+
"async-context",
|
|
11
|
+
"request-context",
|
|
12
|
+
"nodejs"
|
|
13
|
+
],
|
|
14
|
+
"author": "hassanzohdy",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/warlockjs/context.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/warlockjs/context#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/warlockjs/context/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"version": "4.1.2",
|
|
28
|
+
"main": "./cjs/index.cjs",
|
|
29
|
+
"module": "./esm/index.mjs",
|
|
30
|
+
"types": "./esm/index.d.mts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./esm/index.d.mts",
|
|
35
|
+
"default": "./esm/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./esm/index.d.mts",
|
|
39
|
+
"default": "./cjs/index.cjs"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|