@warlock.js/context 5.19.0 → 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
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: orchestrate-contexts
|
|
3
|
-
description:
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Orchestrate contexts — `contextManager`
|
|
7
|
-
|
|
8
|
-
`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.
|
|
9
|
-
|
|
10
|
-
## Why use it
|
|
11
|
-
|
|
12
|
-
You can call `context.run(store, fn)` directly when you only have one context. With two or more, the manager handles the nesting:
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
// ❌ Without the manager — fragile, easy to forget a layer
|
|
16
|
-
await requestContext.run(reqStore, async () =>
|
|
17
|
-
databaseContext.run(dbStore, async () =>
|
|
18
|
-
tenantContext.run(tenantStore, async () => handle()),
|
|
19
|
-
),
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
// ✅ With the manager — one call, deterministic order
|
|
23
|
-
await contextManager.runAll({ request: reqStore, database: dbStore, tenant: tenantStore }, handle);
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Register contexts at boot
|
|
27
|
-
|
|
28
|
-
```ts
|
|
29
|
-
import { contextManager } from "@warlock.js/context";
|
|
30
|
-
import { requestContext } from "./request-context";
|
|
31
|
-
import { databaseContext } from "./database-context";
|
|
32
|
-
import { tenantContext } from "./tenant-context";
|
|
33
|
-
|
|
34
|
-
contextManager
|
|
35
|
-
.register("request", requestContext)
|
|
36
|
-
.register("database", databaseContext)
|
|
37
|
-
.register("tenant", tenantContext);
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Returns the manager — chain registrations. Names must be unique; re-registering with the same name overwrites.
|
|
41
|
-
|
|
42
|
-
## Build stores + run
|
|
43
|
-
|
|
44
|
-
The typical flow is two-step: build initial stores from a request-like payload, then run.
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
app.use(async (req, res, next) => {
|
|
48
|
-
const stores = contextManager.buildStores({
|
|
49
|
-
request: req,
|
|
50
|
-
response: res,
|
|
51
|
-
tenantId: req.headers["x-tenant-id"],
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
await contextManager.runAll(stores, async () => {
|
|
55
|
-
await next();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`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 }`.
|
|
61
|
-
|
|
62
|
-
`runAll(stores, fn)` nests every context's `run()` in registration order, then invokes `fn` at the innermost layer. All contexts are active inside `fn`.
|
|
63
|
-
|
|
64
|
-
## `enterAll()` — middleware without a callback
|
|
65
|
-
|
|
66
|
-
When the framework doesn't give you a callback to wrap (e.g. Express middleware where you call `next()` and return):
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
function contextMiddleware(req, res, next) {
|
|
70
|
-
const stores = contextManager.buildStores({ request: req, response: res });
|
|
71
|
-
contextManager.enterAll(stores);
|
|
72
|
-
next();
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`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.
|
|
77
|
-
|
|
78
|
-
## Lookup + introspection
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
contextManager.hasContext("tenant"); // boolean
|
|
82
|
-
const tenant = contextManager.getContext<TenantContext>("tenant");
|
|
83
|
-
// returns the registered instance or undefined
|
|
84
|
-
contextManager.unregister("debug"); // remove a context
|
|
85
|
-
contextManager.clearAll(); // clear stores on every context
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
`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.
|
|
89
|
-
|
|
90
|
-
## Order matters
|
|
91
|
-
|
|
92
|
-
`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.
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
contextManager
|
|
96
|
-
.register("trace", traceContext) // outermost — runs first
|
|
97
|
-
.register("request", requestContext)
|
|
98
|
-
.register("database", databaseContext)
|
|
99
|
-
.register("tenant", tenantContext); // innermost — runs last, inside all others
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Real-world: multi-tenant request lifecycle
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
import { Context, contextManager } from "@warlock.js/context";
|
|
106
|
-
import { randomUUID } from "crypto";
|
|
107
|
-
|
|
108
|
-
class TraceContext extends Context<{ traceId: string; startTime: number }> {
|
|
109
|
-
public buildStore(): { traceId: string; startTime: number } {
|
|
110
|
-
return { traceId: randomUUID(), startTime: Date.now() };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
public get traceId() {
|
|
114
|
-
return this.get("traceId");
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
class RequestContext extends Context<{ request: any; response: any }> {
|
|
119
|
-
public buildStore(payload?: any) {
|
|
120
|
-
return { request: payload?.request, response: payload?.response };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
class TenantContext extends Context<{ tenantId: string }> {
|
|
125
|
-
public buildStore(payload?: any) {
|
|
126
|
-
return { tenantId: payload?.tenantId ?? "" };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export const traceContext = new TraceContext();
|
|
131
|
-
export const requestContext = new RequestContext();
|
|
132
|
-
export const tenantContext = new TenantContext();
|
|
133
|
-
|
|
134
|
-
contextManager
|
|
135
|
-
.register("trace", traceContext)
|
|
136
|
-
.register("request", requestContext)
|
|
137
|
-
.register("tenant", tenantContext);
|
|
138
|
-
|
|
139
|
-
// In your HTTP layer:
|
|
140
|
-
async function handleRequest(req: any, res: any) {
|
|
141
|
-
const stores = contextManager.buildStores({
|
|
142
|
-
request: req,
|
|
143
|
-
response: res,
|
|
144
|
-
tenantId: req.headers["x-tenant-id"],
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
return contextManager.runAll(stores, async () => {
|
|
148
|
-
// All three contexts active here:
|
|
149
|
-
console.log(`Trace ${traceContext.get("traceId")} — tenant ${tenantContext.get("tenantId")}`);
|
|
150
|
-
|
|
151
|
-
await routeAndDispatch(req, res);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Things NOT to do
|
|
157
|
-
|
|
158
|
-
- 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).
|
|
159
|
-
- 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.
|
|
160
|
-
- Don't use the manager when only one context applies. `context.run(store, fn)` is shorter and has the same semantics.
|
|
161
|
-
- 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.
|
|
1
|
+
---
|
|
2
|
+
name: orchestrate-contexts
|
|
3
|
+
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.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Orchestrate contexts — `contextManager`
|
|
7
|
+
|
|
8
|
+
`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.
|
|
9
|
+
|
|
10
|
+
## Why use it
|
|
11
|
+
|
|
12
|
+
You can call `context.run(store, fn)` directly when you only have one context. With two or more, the manager handles the nesting:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// ❌ Without the manager — fragile, easy to forget a layer
|
|
16
|
+
await requestContext.run(reqStore, async () =>
|
|
17
|
+
databaseContext.run(dbStore, async () =>
|
|
18
|
+
tenantContext.run(tenantStore, async () => handle()),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// ✅ With the manager — one call, deterministic order
|
|
23
|
+
await contextManager.runAll({ request: reqStore, database: dbStore, tenant: tenantStore }, handle);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Register contexts at boot
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { contextManager } from "@warlock.js/context";
|
|
30
|
+
import { requestContext } from "./request-context";
|
|
31
|
+
import { databaseContext } from "./database-context";
|
|
32
|
+
import { tenantContext } from "./tenant-context";
|
|
33
|
+
|
|
34
|
+
contextManager
|
|
35
|
+
.register("request", requestContext)
|
|
36
|
+
.register("database", databaseContext)
|
|
37
|
+
.register("tenant", tenantContext);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Returns the manager — chain registrations. Names must be unique; re-registering with the same name overwrites.
|
|
41
|
+
|
|
42
|
+
## Build stores + run
|
|
43
|
+
|
|
44
|
+
The typical flow is two-step: build initial stores from a request-like payload, then run.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
app.use(async (req, res, next) => {
|
|
48
|
+
const stores = contextManager.buildStores({
|
|
49
|
+
request: req,
|
|
50
|
+
response: res,
|
|
51
|
+
tenantId: req.headers["x-tenant-id"],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await contextManager.runAll(stores, async () => {
|
|
55
|
+
await next();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`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 }`.
|
|
61
|
+
|
|
62
|
+
`runAll(stores, fn)` nests every context's `run()` in registration order, then invokes `fn` at the innermost layer. All contexts are active inside `fn`.
|
|
63
|
+
|
|
64
|
+
## `enterAll()` — middleware without a callback
|
|
65
|
+
|
|
66
|
+
When the framework doesn't give you a callback to wrap (e.g. Express middleware where you call `next()` and return):
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
function contextMiddleware(req, res, next) {
|
|
70
|
+
const stores = contextManager.buildStores({ request: req, response: res });
|
|
71
|
+
contextManager.enterAll(stores);
|
|
72
|
+
next();
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`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.
|
|
77
|
+
|
|
78
|
+
## Lookup + introspection
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
contextManager.hasContext("tenant"); // boolean
|
|
82
|
+
const tenant = contextManager.getContext<TenantContext>("tenant");
|
|
83
|
+
// returns the registered instance or undefined
|
|
84
|
+
contextManager.unregister("debug"); // remove a context
|
|
85
|
+
contextManager.clearAll(); // clear stores on every context
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`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.
|
|
89
|
+
|
|
90
|
+
## Order matters
|
|
91
|
+
|
|
92
|
+
`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.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
contextManager
|
|
96
|
+
.register("trace", traceContext) // outermost — runs first
|
|
97
|
+
.register("request", requestContext)
|
|
98
|
+
.register("database", databaseContext)
|
|
99
|
+
.register("tenant", tenantContext); // innermost — runs last, inside all others
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Real-world: multi-tenant request lifecycle
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { Context, contextManager } from "@warlock.js/context";
|
|
106
|
+
import { randomUUID } from "crypto";
|
|
107
|
+
|
|
108
|
+
class TraceContext extends Context<{ traceId: string; startTime: number }> {
|
|
109
|
+
public buildStore(): { traceId: string; startTime: number } {
|
|
110
|
+
return { traceId: randomUUID(), startTime: Date.now() };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public get traceId() {
|
|
114
|
+
return this.get("traceId");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class RequestContext extends Context<{ request: any; response: any }> {
|
|
119
|
+
public buildStore(payload?: any) {
|
|
120
|
+
return { request: payload?.request, response: payload?.response };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class TenantContext extends Context<{ tenantId: string }> {
|
|
125
|
+
public buildStore(payload?: any) {
|
|
126
|
+
return { tenantId: payload?.tenantId ?? "" };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const traceContext = new TraceContext();
|
|
131
|
+
export const requestContext = new RequestContext();
|
|
132
|
+
export const tenantContext = new TenantContext();
|
|
133
|
+
|
|
134
|
+
contextManager
|
|
135
|
+
.register("trace", traceContext)
|
|
136
|
+
.register("request", requestContext)
|
|
137
|
+
.register("tenant", tenantContext);
|
|
138
|
+
|
|
139
|
+
// In your HTTP layer:
|
|
140
|
+
async function handleRequest(req: any, res: any) {
|
|
141
|
+
const stores = contextManager.buildStores({
|
|
142
|
+
request: req,
|
|
143
|
+
response: res,
|
|
144
|
+
tenantId: req.headers["x-tenant-id"],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return contextManager.runAll(stores, async () => {
|
|
148
|
+
// All three contexts active here:
|
|
149
|
+
console.log(`Trace ${traceContext.get("traceId")} — tenant ${tenantContext.get("tenantId")}`);
|
|
150
|
+
|
|
151
|
+
await routeAndDispatch(req, res);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Things NOT to do
|
|
157
|
+
|
|
158
|
+
- 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).
|
|
159
|
+
- 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.
|
|
160
|
+
- Don't use the manager when only one context applies. `context.run(store, fn)` is shorter and has the same semantics.
|
|
161
|
+
- 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.
|