@warlock.js/context 5.19.1 → 5.20.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/CHANGELOG.md +6 -0
- package/llms-full.txt +321 -321
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/skills/define-context/SKILL.md +160 -160
- package/skills/index.md +19 -0
- package/skills/orchestrate-contexts/SKILL.md +161 -161
package/llms.txt
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [define-context](@warlock.js/context/define-context/SKILL.md):
|
|
10
|
-
- [orchestrate-contexts](@warlock.js/context/orchestrate-contexts/SKILL.md):
|
|
9
|
+
- [define-context](@warlock.js/context/define-context/SKILL.md): (no description)
|
|
10
|
+
- [orchestrate-contexts](@warlock.js/context/orchestrate-contexts/SKILL.md): (no description)
|
|
11
11
|
- [overview](@warlock.js/context/overview/SKILL.md): @warlock.js/context overview for orientation and choosing the focused skill for the task.
|
package/package.json
CHANGED
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: define-context
|
|
3
|
-
description:
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Define a context
|
|
7
|
-
|
|
8
|
-
`@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.
|
|
9
|
-
|
|
10
|
-
## Install
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm install @warlock.js/context
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Shape
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { Context } from "@warlock.js/context";
|
|
20
|
-
|
|
21
|
-
interface UserContextStore {
|
|
22
|
-
userId: string;
|
|
23
|
-
role: "admin" | "user";
|
|
24
|
-
tenantId: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
class UserContext extends Context<UserContextStore> {
|
|
28
|
-
/**
|
|
29
|
-
* Called by `contextManager.buildStores(payload)` for each registered context.
|
|
30
|
-
* Override to provide initialization logic for this context's store.
|
|
31
|
-
*/
|
|
32
|
-
public buildStore(payload?: Record<string, any>): UserContextStore {
|
|
33
|
-
return {
|
|
34
|
-
userId: payload?.userId ?? "",
|
|
35
|
-
role: payload?.role ?? "user",
|
|
36
|
-
tenantId: payload?.tenantId ?? "",
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const userContext = new UserContext();
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
`buildStore` is the **only** abstract method. Everything else is provided by `Context<TStore>`.
|
|
45
|
-
|
|
46
|
-
## Usage modes
|
|
47
|
-
|
|
48
|
-
### `run()` — scoped execution
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
await userContext.run(
|
|
52
|
-
{ userId: "123", role: "admin", tenantId: "acme" },
|
|
53
|
-
async () => {
|
|
54
|
-
// Context is available throughout this async scope and any awaited calls inside it.
|
|
55
|
-
const userId = userContext.get("userId"); // "123"
|
|
56
|
-
const role = userContext.get("role"); // "admin"
|
|
57
|
-
|
|
58
|
-
await someAsyncOperation(); // context propagates through awaits
|
|
59
|
-
},
|
|
60
|
-
);
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
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.
|
|
64
|
-
|
|
65
|
-
### `enter()` — middleware-style, no callback
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
function authMiddleware(req, res, next) {
|
|
69
|
-
userContext.enter({
|
|
70
|
-
userId: req.user.id,
|
|
71
|
-
role: req.user.role,
|
|
72
|
-
tenantId: req.headers["x-tenant-id"],
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
next(); // context lives for the rest of the request
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Use `enter()` when the framework doesn't give you a callback to wrap (Express-style middleware). Under the hood it's `AsyncLocalStorage.enterWith(store)`.
|
|
80
|
-
|
|
81
|
-
### `update()` — merge into the current context
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
userContext.update({ role: "admin" });
|
|
85
|
-
// existing store: { userId, role, tenantId } → { userId, role: "admin", tenantId }
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
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.
|
|
89
|
-
|
|
90
|
-
`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.
|
|
91
|
-
|
|
92
|
-
## Reading
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
const userId = userContext.get("userId"); // TStore[K] | undefined
|
|
96
|
-
const store = userContext.getStore(); // TStore | undefined
|
|
97
|
-
const inside = userContext.hasContext(); // boolean
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`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).
|
|
101
|
-
|
|
102
|
-
## Writing within a context
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
userContext.set("role", "admin"); // sugar for update({ role: "admin" })
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want.
|
|
109
|
-
|
|
110
|
-
## Clearing
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
userContext.clear();
|
|
114
|
-
// replaces the current store with an empty object of TStore
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Rare in app code. The auto-cleanup at the end of `run()` is the normal path.
|
|
118
|
-
|
|
119
|
-
## Convenience getters via subclass
|
|
120
|
-
|
|
121
|
-
Add domain-friendly getters on the subclass when a key is read a lot:
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
class TenantContext extends Context<TenantStore> {
|
|
125
|
-
public buildStore(payload?: Record<string, any>): TenantStore {
|
|
126
|
-
return {
|
|
127
|
-
tenantId: payload?.tenantId ?? "",
|
|
128
|
-
tenantName: payload?.tenantName ?? "",
|
|
129
|
-
config: payload?.config ?? {},
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
public get tenantId() {
|
|
134
|
-
return this.get("tenantId");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
public get config() {
|
|
138
|
-
return this.get("config");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Now `tenantContext.tenantId` reads better than `tenantContext.get("tenantId")` at the call site.
|
|
144
|
-
|
|
145
|
-
## What it's NOT for
|
|
146
|
-
|
|
147
|
-
- **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.
|
|
148
|
-
- **Thread-safe shared mutable state.** Each `run()` gets a fresh store. Two parallel `run()` calls don't see each other's updates.
|
|
149
|
-
- **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.
|
|
150
|
-
|
|
151
|
-
## See also
|
|
152
|
-
|
|
153
|
-
- [`@warlock.js/context/orchestrate-contexts/SKILL.md`](@warlock.js/context/orchestrate-contexts/SKILL.md) — running multiple contexts together via the `contextManager` singleton.
|
|
154
|
-
|
|
155
|
-
## Things NOT to do
|
|
156
|
-
|
|
157
|
-
- 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.
|
|
158
|
-
- 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.
|
|
159
|
-
- Don't mutate the store object directly without `update`/`set` — works, but obscures intent. The methods exist to make state changes searchable.
|
|
160
|
-
- Don't share one context instance across unrelated concerns. One typed context per domain concept reads better than one fat `globalContext`.
|
|
1
|
+
---
|
|
2
|
+
name: define-context
|
|
3
|
+
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.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Define a context
|
|
7
|
+
|
|
8
|
+
`@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.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @warlock.js/context
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Shape
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Context } from "@warlock.js/context";
|
|
20
|
+
|
|
21
|
+
interface UserContextStore {
|
|
22
|
+
userId: string;
|
|
23
|
+
role: "admin" | "user";
|
|
24
|
+
tenantId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class UserContext extends Context<UserContextStore> {
|
|
28
|
+
/**
|
|
29
|
+
* Called by `contextManager.buildStores(payload)` for each registered context.
|
|
30
|
+
* Override to provide initialization logic for this context's store.
|
|
31
|
+
*/
|
|
32
|
+
public buildStore(payload?: Record<string, any>): UserContextStore {
|
|
33
|
+
return {
|
|
34
|
+
userId: payload?.userId ?? "",
|
|
35
|
+
role: payload?.role ?? "user",
|
|
36
|
+
tenantId: payload?.tenantId ?? "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const userContext = new UserContext();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`buildStore` is the **only** abstract method. Everything else is provided by `Context<TStore>`.
|
|
45
|
+
|
|
46
|
+
## Usage modes
|
|
47
|
+
|
|
48
|
+
### `run()` — scoped execution
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await userContext.run(
|
|
52
|
+
{ userId: "123", role: "admin", tenantId: "acme" },
|
|
53
|
+
async () => {
|
|
54
|
+
// Context is available throughout this async scope and any awaited calls inside it.
|
|
55
|
+
const userId = userContext.get("userId"); // "123"
|
|
56
|
+
const role = userContext.get("role"); // "admin"
|
|
57
|
+
|
|
58
|
+
await someAsyncOperation(); // context propagates through awaits
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
### `enter()` — middleware-style, no callback
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
function authMiddleware(req, res, next) {
|
|
69
|
+
userContext.enter({
|
|
70
|
+
userId: req.user.id,
|
|
71
|
+
role: req.user.role,
|
|
72
|
+
tenantId: req.headers["x-tenant-id"],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
next(); // context lives for the rest of the request
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use `enter()` when the framework doesn't give you a callback to wrap (Express-style middleware). Under the hood it's `AsyncLocalStorage.enterWith(store)`.
|
|
80
|
+
|
|
81
|
+
### `update()` — merge into the current context
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
userContext.update({ role: "admin" });
|
|
85
|
+
// existing store: { userId, role, tenantId } → { userId, role: "admin", tenantId }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
`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.
|
|
91
|
+
|
|
92
|
+
## Reading
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const userId = userContext.get("userId"); // TStore[K] | undefined
|
|
96
|
+
const store = userContext.getStore(); // TStore | undefined
|
|
97
|
+
const inside = userContext.hasContext(); // boolean
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`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).
|
|
101
|
+
|
|
102
|
+
## Writing within a context
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
userContext.set("role", "admin"); // sugar for update({ role: "admin" })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Only call `set` inside an active context. Outside one, it enters a new context with just that key set — usually not what you want.
|
|
109
|
+
|
|
110
|
+
## Clearing
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
userContext.clear();
|
|
114
|
+
// replaces the current store with an empty object of TStore
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Rare in app code. The auto-cleanup at the end of `run()` is the normal path.
|
|
118
|
+
|
|
119
|
+
## Convenience getters via subclass
|
|
120
|
+
|
|
121
|
+
Add domain-friendly getters on the subclass when a key is read a lot:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
class TenantContext extends Context<TenantStore> {
|
|
125
|
+
public buildStore(payload?: Record<string, any>): TenantStore {
|
|
126
|
+
return {
|
|
127
|
+
tenantId: payload?.tenantId ?? "",
|
|
128
|
+
tenantName: payload?.tenantName ?? "",
|
|
129
|
+
config: payload?.config ?? {},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public get tenantId() {
|
|
134
|
+
return this.get("tenantId");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public get config() {
|
|
138
|
+
return this.get("config");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Now `tenantContext.tenantId` reads better than `tenantContext.get("tenantId")` at the call site.
|
|
144
|
+
|
|
145
|
+
## What it's NOT for
|
|
146
|
+
|
|
147
|
+
- **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.
|
|
148
|
+
- **Thread-safe shared mutable state.** Each `run()` gets a fresh store. Two parallel `run()` calls don't see each other's updates.
|
|
149
|
+
- **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.
|
|
150
|
+
|
|
151
|
+
## See also
|
|
152
|
+
|
|
153
|
+
- [`@warlock.js/context/orchestrate-contexts/SKILL.md`](@warlock.js/context/orchestrate-contexts/SKILL.md) — running multiple contexts together via the `contextManager` singleton.
|
|
154
|
+
|
|
155
|
+
## Things NOT to do
|
|
156
|
+
|
|
157
|
+
- 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.
|
|
158
|
+
- 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.
|
|
159
|
+
- Don't mutate the store object directly without `update`/`set` — works, but obscures intent. The methods exist to make state changes searchable.
|
|
160
|
+
- Don't share one context instance across unrelated concerns. One typed context per domain concept reads better than one fat `globalContext`.
|
package/skills/index.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Typed request-scoped context on Node AsyncLocalStorage, so data (user, tenant, trace id, transaction) flows through awaits without parameter threading. Exports `Context`, `contextManager`, `ContextManager`. Use for: \"share the current user or tenant across async calls\", \"request-scoped storage\", \"AsyncLocalStorage wrapper\", \"run several contexts together\", \"avoid passing data through every function\". Not this package: cross-request state → @warlock.js/cache or a database; HTTP request/response objects → @warlock.js/core."
|
|
3
|
+
---
|
|
4
|
+
# @warlock.js/context
|
|
5
|
+
|
|
6
|
+
A thin typed wrapper over `AsyncLocalStorage`. Each `Context<TStore>` subclass declares its store shape and how `buildStore` creates it; the store lives for the scope and propagates through every `await` inside. `contextManager` runs several contexts together. Zero dependencies; the framework uses it internally for request, user and tenant.
|
|
7
|
+
|
|
8
|
+
## The 80% path
|
|
9
|
+
1. Orient with `overview.md`.
|
|
10
|
+
2. Subclass `Context`, implement `buildStore`, read with `get` / `set` anywhere down the call tree: `define-context.md`.
|
|
11
|
+
3. Wrap work with `run(store, callback)`, or `enter(store)` for middleware that cannot wrap a callback: `define-context.md`.
|
|
12
|
+
4. With two or more active contexts, register them on `contextManager` and use `runAll` / `enterAll`: `orchestrate-contexts.md`.
|
|
13
|
+
|
|
14
|
+
## Conventions and pitfalls
|
|
15
|
+
- Outside a `run`/`enter` scope there is no store; guard with `hasContext` when code may run unscoped.
|
|
16
|
+
- `enter` leaks until cleared; pair `enterAll` with `clearAll` at the end of the request.
|
|
17
|
+
- Worker threads do not share the store; serialize data across and re-enter.
|
|
18
|
+
- Context is not shared state: it vanishes with the scope.
|
|
19
|
+
- One context in a cooperative framework needs no manager; use plain `run`.
|