@maestro-js/context 1.0.0-alpha.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/README.md +360 -0
- package/dist/index.d.ts +181 -0
- package/dist/index.js +143 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# @maestro-js/context
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `@maestro-js/context` package provides request-scoped key-value storage backed by Node.js `AsyncLocalStorage`. Each call to `run()` creates an isolated store with two buckets -- visible (safe for logging) and hidden (for sensitive data like tokens) -- ensuring data never leaks between concurrent requests.
|
|
6
|
+
|
|
7
|
+
## Quick Setup
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Context } from '@maestro-js/context'
|
|
11
|
+
|
|
12
|
+
// Create and register a context provider
|
|
13
|
+
const ctx = Context.Provider.create()
|
|
14
|
+
Context.Provider.register('default', ctx)
|
|
15
|
+
|
|
16
|
+
// Use the default facade directly
|
|
17
|
+
Context.run(() => {
|
|
18
|
+
Context.add('userId', 123)
|
|
19
|
+
Context.get<number>('userId') // 123
|
|
20
|
+
Context.all() // { userId: 123 }
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API Reference
|
|
25
|
+
|
|
26
|
+
### Lifecycle Methods
|
|
27
|
+
|
|
28
|
+
#### `run<T>(callback: () => T): T`
|
|
29
|
+
|
|
30
|
+
Initialize a fresh context store for the duration of `callback`. All `add`/`get`/`push` calls inside the callback (including across `await` boundaries) share the same isolated store. Returns the callback's return value.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const result = Context.run(() => {
|
|
34
|
+
Context.add('requestId', 'abc-123')
|
|
35
|
+
return Context.get('requestId')
|
|
36
|
+
})
|
|
37
|
+
// result === 'abc-123'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### `runWith<T>(store: Context.Store | undefined, callback: () => T): T`
|
|
41
|
+
|
|
42
|
+
Run `callback` within a previously captured store so context is available inside. If `store` is `undefined`, run the callback without any context (all reads return `undefined`).
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
Context.run(() => {
|
|
46
|
+
Context.add('userId', 42)
|
|
47
|
+
const store = Context.getStore()
|
|
48
|
+
|
|
49
|
+
// Later, in a different async boundary:
|
|
50
|
+
Context.runWith(store, () => {
|
|
51
|
+
Context.get('userId') // 42
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### `getStore(): Context.Store | undefined`
|
|
57
|
+
|
|
58
|
+
Return a reference to the current `AsyncLocalStorage` store, or `undefined` if called outside a `run()`. The returned store is a live reference -- mutations via `runWith` affect the original.
|
|
59
|
+
|
|
60
|
+
### Visible Context Methods
|
|
61
|
+
|
|
62
|
+
#### `add(key: Context.Key, value: any): void`
|
|
63
|
+
|
|
64
|
+
Store a key-value pair in the visible context, overriding any existing value.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
Context.run(() => {
|
|
68
|
+
Context.add('tenantId', 'acme-corp')
|
|
69
|
+
Context.add('tenantId', 'other-corp') // overrides
|
|
70
|
+
Context.get('tenantId') // 'other-corp'
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### `get<Value = unknown>(key: Context.Key): Value | undefined`
|
|
75
|
+
|
|
76
|
+
Retrieve the value for a key from the visible context, or `undefined` if absent.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
Context.run(() => {
|
|
80
|
+
Context.add('role', 'admin')
|
|
81
|
+
Context.get<string>('role') // 'admin'
|
|
82
|
+
Context.get('nonexistent') // undefined
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `has(key: Context.Key): boolean`
|
|
87
|
+
|
|
88
|
+
Return `true` if the key exists in the visible context.
|
|
89
|
+
|
|
90
|
+
#### `del(...key: Context.Key[]): void`
|
|
91
|
+
|
|
92
|
+
Remove one or more keys from the visible context.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
Context.run(() => {
|
|
96
|
+
Context.add('a', 1)
|
|
97
|
+
Context.add('b', 2)
|
|
98
|
+
Context.del('a', 'b')
|
|
99
|
+
Context.has('a') // false
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `push(key: Context.Key, ...value: any[]): void`
|
|
104
|
+
|
|
105
|
+
Append value(s) to an array in the visible context. Create the array if the key does not exist. Wrap an existing non-array value into an array before appending.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
Context.run(() => {
|
|
109
|
+
Context.push('tags', 'admin')
|
|
110
|
+
Context.get('tags') // ['admin']
|
|
111
|
+
|
|
112
|
+
Context.push('tags', 'user', 'verified')
|
|
113
|
+
Context.get('tags') // ['admin', 'user', 'verified']
|
|
114
|
+
|
|
115
|
+
Context.add('scalar', 'x')
|
|
116
|
+
Context.push('scalar', 'y')
|
|
117
|
+
Context.get('scalar') // ['x', 'y']
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `all(): { [key: Context.Key]: unknown }`
|
|
122
|
+
|
|
123
|
+
Return all visible context entries as a plain object. Hidden entries are excluded.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
Context.run(() => {
|
|
127
|
+
Context.add('userId', 123)
|
|
128
|
+
Context.addHidden('authToken', 'secret')
|
|
129
|
+
Context.all() // { userId: 123 }
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `createItem<Value = unknown>(key: Context.Key)`
|
|
134
|
+
|
|
135
|
+
Return a typed handle scoped to a single key in the visible context. The handle exposes `add(value)`, `get()`, `del()`, and `push(...)` methods.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
Context.run(() => {
|
|
139
|
+
const userId = Context.createItem<number>('userId')
|
|
140
|
+
userId.add(42)
|
|
141
|
+
userId.get() // 42
|
|
142
|
+
userId.del()
|
|
143
|
+
userId.get() // undefined
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Hidden Context Methods
|
|
148
|
+
|
|
149
|
+
Hidden methods mirror visible methods but operate on a separate bucket excluded from `all()`. Use hidden context for sensitive data (tokens, credentials) that should not appear in logs.
|
|
150
|
+
|
|
151
|
+
- `addHidden(key, value)` -- Store in hidden context.
|
|
152
|
+
- `getHidden<Value>(key)` -- Retrieve from hidden context.
|
|
153
|
+
- `hasHidden(key)` -- Check existence in hidden context.
|
|
154
|
+
- `delHidden(...key)` -- Remove from hidden context.
|
|
155
|
+
- `pushHidden(key, ...value)` -- Append to array in hidden context.
|
|
156
|
+
- `allHidden()` -- Return all hidden entries as a plain object.
|
|
157
|
+
- `createHiddenItem<Value>(key)` -- Typed handle for hidden context.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
Context.run(() => {
|
|
161
|
+
Context.addHidden('authToken', 'Bearer xyz')
|
|
162
|
+
Context.getHidden('authToken') // 'Bearer xyz'
|
|
163
|
+
Context.all() // {} -- hidden values excluded
|
|
164
|
+
Context.allHidden() // { authToken: 'Bearer xyz' }
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Provider Methods
|
|
169
|
+
|
|
170
|
+
#### `Context.Provider.create(): Context.ContextService`
|
|
171
|
+
|
|
172
|
+
Factory that instantiates a new context service with its own `AsyncLocalStorage`.
|
|
173
|
+
|
|
174
|
+
#### `Context.Provider.register(name: Context.Provider.Key, service: Context.ContextService): void`
|
|
175
|
+
|
|
176
|
+
Register an instance in the named registry.
|
|
177
|
+
|
|
178
|
+
#### `Context.provider(key: Context.Provider.Key): Context.ContextService`
|
|
179
|
+
|
|
180
|
+
Resolve a named instance. Resolution is deferred via Proxy, so `provider()` can be called before `register()`.
|
|
181
|
+
|
|
182
|
+
## Common Patterns
|
|
183
|
+
|
|
184
|
+
### Request-Scoped Data in HTTP Middleware
|
|
185
|
+
|
|
186
|
+
Wrap each incoming request in `Context.run()` to isolate per-request data:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
function contextMiddleware(req, res, next) {
|
|
190
|
+
Context.run(() => {
|
|
191
|
+
Context.add('requestId', req.headers['x-request-id'])
|
|
192
|
+
Context.add('method', req.method)
|
|
193
|
+
Context.add('path', req.path)
|
|
194
|
+
Context.addHidden('authToken', req.headers.authorization)
|
|
195
|
+
next()
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Propagating Context Across Async Boundaries
|
|
201
|
+
|
|
202
|
+
When context must survive into a callback that runs outside the original `run()` scope (e.g., stream event handlers), capture and restore the store:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
Context.run(() => {
|
|
206
|
+
Context.add('queryId', 'q-001')
|
|
207
|
+
const store = Context.getStore()
|
|
208
|
+
|
|
209
|
+
stream.on('end', () => {
|
|
210
|
+
Context.runWith(store, () => {
|
|
211
|
+
// Context is available here
|
|
212
|
+
Context.get('queryId') // 'q-001'
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This pattern is used by `@maestro-js/db` for query stream logging and by `@maestro-js/tracing` for span instrumentation.
|
|
219
|
+
|
|
220
|
+
### Nested Runs Create Fresh Stores
|
|
221
|
+
|
|
222
|
+
Each `run()` creates a completely fresh store. Nested calls do not inherit from the outer scope:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
Context.run(() => {
|
|
226
|
+
Context.add('outer', 1)
|
|
227
|
+
Context.run(() => {
|
|
228
|
+
Context.get('outer') // undefined -- fresh store
|
|
229
|
+
Context.add('inner', 2)
|
|
230
|
+
})
|
|
231
|
+
Context.get('inner') // undefined -- inner store is gone
|
|
232
|
+
Context.get('outer') // 1
|
|
233
|
+
})
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Silent No-Ops Outside run()
|
|
237
|
+
|
|
238
|
+
All context operations silently do nothing when called outside a `run()` scope. No errors are thrown:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
Context.add('key', 'value') // no-op
|
|
242
|
+
Context.get('key') // undefined
|
|
243
|
+
Context.has('key') // false
|
|
244
|
+
Context.all() // {}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Store Mutations via runWith Are Shared
|
|
248
|
+
|
|
249
|
+
`getStore()` returns a reference, not a copy. Mutations inside `runWith` affect the original store:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
Context.run(() => {
|
|
253
|
+
Context.add('count', 1)
|
|
254
|
+
const store = Context.getStore()
|
|
255
|
+
|
|
256
|
+
Context.runWith(store, () => {
|
|
257
|
+
Context.add('count', 2)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
Context.get('count') // 2 -- mutated by runWith
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Multiple Isolated Providers
|
|
265
|
+
|
|
266
|
+
Separate `create()` calls produce fully independent context instances:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const appContext = Context.Provider.create()
|
|
270
|
+
const requestContext = Context.Provider.create()
|
|
271
|
+
|
|
272
|
+
appContext.run(() => {
|
|
273
|
+
appContext.add('env', 'production')
|
|
274
|
+
requestContext.run(() => {
|
|
275
|
+
requestContext.add('env', 'staging')
|
|
276
|
+
appContext.get('env') // 'production'
|
|
277
|
+
requestContext.get('env') // 'staging'
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Testing
|
|
283
|
+
|
|
284
|
+
Context requires no mock driver -- `Provider.create()` returns a fully functional instance using in-process `AsyncLocalStorage`. Wrap test logic in `run()`:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { Context } from '@maestro-js/context'
|
|
288
|
+
import { test } from 'beartest-js'
|
|
289
|
+
import { expect } from 'expect'
|
|
290
|
+
|
|
291
|
+
test('adds and retrieves context values', () => {
|
|
292
|
+
const ctx = Context.Provider.create()
|
|
293
|
+
ctx.run(() => {
|
|
294
|
+
ctx.add('userId', 123)
|
|
295
|
+
expect(ctx.get('userId')).toBe(123)
|
|
296
|
+
expect(ctx.has('userId')).toBe(true)
|
|
297
|
+
expect(ctx.all()).toEqual({ userId: 123 })
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('hidden values excluded from all()', () => {
|
|
302
|
+
const ctx = Context.Provider.create()
|
|
303
|
+
ctx.run(() => {
|
|
304
|
+
ctx.add('visible', 'yes')
|
|
305
|
+
ctx.addHidden('secret', 'token')
|
|
306
|
+
expect(ctx.all()).toEqual({ visible: 'yes' })
|
|
307
|
+
expect(ctx.getHidden('secret')).toBe('token')
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('async propagation across await', async () => {
|
|
312
|
+
const ctx = Context.Provider.create()
|
|
313
|
+
await ctx.run(async () => {
|
|
314
|
+
ctx.add('before', true)
|
|
315
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
316
|
+
expect(ctx.get('before')).toBe(true)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Run tests with:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
pnpm --filter @maestro-js/context test
|
|
325
|
+
# or
|
|
326
|
+
cd packages/context && npx beartest ./tests/context.test.ts
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Cross-Package Integration
|
|
330
|
+
|
|
331
|
+
### Db (query stream logging)
|
|
332
|
+
|
|
333
|
+
The `@maestro-js/db` package captures the context store before creating query streams and restores it in `end` and `error` event handlers via `Context.runWith()`, ensuring log messages emitted during stream callbacks include the original request context.
|
|
334
|
+
|
|
335
|
+
### Tracing (span instrumentation)
|
|
336
|
+
|
|
337
|
+
The `@maestro-js/tracing` package captures the context store when starting or wrapping spans and restores it in `onEnd` callbacks via `Context.runWith()`, so span completion logs include the request context.
|
|
338
|
+
|
|
339
|
+
### Log (contextual logging)
|
|
340
|
+
|
|
341
|
+
Log transports can call `Context.all()` to attach visible context entries to every log line within a request scope, providing automatic request-scoped structured logging without manually passing data.
|
|
342
|
+
|
|
343
|
+
### Dependencies
|
|
344
|
+
|
|
345
|
+
- **Depends on**: `@maestro-js/service-registry` (Provider pattern foundation)
|
|
346
|
+
- **Used by**: `@maestro-js/db`, `@maestro-js/tracing`
|
|
347
|
+
- **Works with**: `@maestro-js/log` (contextual logging)
|
|
348
|
+
|
|
349
|
+
## Types
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// Key type for context entries
|
|
353
|
+
type Context.Key = string | number | symbol
|
|
354
|
+
|
|
355
|
+
// The raw context snapshot containing visible and hidden Map buckets
|
|
356
|
+
type Context.Store = {
|
|
357
|
+
hidden: Map<Context.Key, unknown>
|
|
358
|
+
visible: Map<Context.Key, unknown>
|
|
359
|
+
}
|
|
360
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a new Context service instance with its own AsyncLocalStorage.
|
|
3
|
+
*/
|
|
4
|
+
declare function create(): Context.ContextService;
|
|
5
|
+
declare function provider(key: Context.Provider.Key): Context.ContextService;
|
|
6
|
+
type KeysWithFallback = keyof Context.Provider.Keys extends never ? {
|
|
7
|
+
default: unknown;
|
|
8
|
+
} : Context.Provider.Keys & {
|
|
9
|
+
default: unknown;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Request-scoped key-value storage backed by AsyncLocalStorage.
|
|
13
|
+
* Call `Context.Provider.create()` and register it, or use the default instance.
|
|
14
|
+
*/
|
|
15
|
+
declare const Context: {
|
|
16
|
+
provider: typeof provider;
|
|
17
|
+
/** Initializes a fresh context store for the duration of `callback`. */
|
|
18
|
+
run<T>(callback: () => T): T;
|
|
19
|
+
/** Stores a key-value pair in the visible context, overriding any existing value. */
|
|
20
|
+
add(key: Context.Key, value: any): void;
|
|
21
|
+
/** Removes one or more keys from the visible context. */
|
|
22
|
+
del(...key: Context.Key[]): void;
|
|
23
|
+
/** Appends value(s) to an array in the visible context. Creates the array if the key doesn't exist. */
|
|
24
|
+
push(key: Context.Key, ...value: any[]): void;
|
|
25
|
+
/** Returns `true` if the key exists in the visible context. */
|
|
26
|
+
has(key: Context.Key): boolean;
|
|
27
|
+
/** Retrieves the value for `key` from the visible context, or `undefined` if absent. */
|
|
28
|
+
get<Value = unknown>(key: Context.Key): Value | undefined;
|
|
29
|
+
/** Returns all visible context entries as a plain object. */
|
|
30
|
+
all(): {
|
|
31
|
+
[key: Context.Key]: unknown;
|
|
32
|
+
};
|
|
33
|
+
/** Returns convenience methods scoped to a single key in the visible context. */
|
|
34
|
+
createItem<Value = unknown>(key: Context.Key): {
|
|
35
|
+
add(value: Value): void;
|
|
36
|
+
del(): void;
|
|
37
|
+
push: Value extends Array<any> ? (...value: Value) => void : never;
|
|
38
|
+
get(): Value | undefined;
|
|
39
|
+
};
|
|
40
|
+
/** Stores a key-value pair in the hidden context (excluded from {@link all}). */
|
|
41
|
+
addHidden(key: Context.Key, value: any): void;
|
|
42
|
+
/** Removes one or more keys from the hidden context. */
|
|
43
|
+
delHidden(...key: Context.Key[]): void;
|
|
44
|
+
/** Appends value(s) to an array in the hidden context. */
|
|
45
|
+
pushHidden(key: Context.Key, ...value: any[]): void;
|
|
46
|
+
/** Returns `true` if the key exists in the hidden context. */
|
|
47
|
+
hasHidden(key: Context.Key): boolean;
|
|
48
|
+
/** Retrieves the value for `key` from the hidden context, or `undefined` if absent. */
|
|
49
|
+
getHidden<Value = unknown>(key: Context.Key): Value | undefined;
|
|
50
|
+
/** Returns all hidden context entries as a plain object. */
|
|
51
|
+
allHidden(): {
|
|
52
|
+
[key: Context.Key]: unknown;
|
|
53
|
+
};
|
|
54
|
+
/** Returns convenience methods scoped to a single key in the hidden context. */
|
|
55
|
+
createHiddenItem<Value = unknown>(key: Context.Key): {
|
|
56
|
+
add(value: Value): void;
|
|
57
|
+
del(): void;
|
|
58
|
+
push: Value extends Array<any> ? (...value: Value) => void : never;
|
|
59
|
+
get(): Value | undefined;
|
|
60
|
+
};
|
|
61
|
+
/** Runs `callback` within the given store so context is available inside. */
|
|
62
|
+
runWith<T>(store: Context.Store | undefined, callback: () => T): T;
|
|
63
|
+
/** Returns a reference to the current AsyncLocalStorage store, or `undefined` outside a `run()`. */
|
|
64
|
+
getStore(): Context.Store | undefined;
|
|
65
|
+
Provider: {
|
|
66
|
+
create: typeof create;
|
|
67
|
+
register: (name: Context.Provider.Key, item: Context.ContextService) => void;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Request-scoped key-value storage backed by AsyncLocalStorage.
|
|
72
|
+
*
|
|
73
|
+
* Values are either "visible" (included when calling `all()`) or "hidden"
|
|
74
|
+
* (accessible only through the `*Hidden` methods, excluded from logging).
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* Context.run(() => {
|
|
79
|
+
* Context.add('userId', 123)
|
|
80
|
+
* Context.push('tags', 'admin', 'user')
|
|
81
|
+
*
|
|
82
|
+
* Context.get<number>('userId') // 123
|
|
83
|
+
* Context.all() // { userId: 123, tags: ['admin', 'user'] }
|
|
84
|
+
*
|
|
85
|
+
* // Typed handle for a single key
|
|
86
|
+
* const userId = Context.createItem<number>('userId')
|
|
87
|
+
* userId.get() // 123
|
|
88
|
+
*
|
|
89
|
+
* // Transfer context to another async boundary
|
|
90
|
+
* const store = Context.getStore()
|
|
91
|
+
* Context.runWith(store, () => { ... })
|
|
92
|
+
* })
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare namespace Context {
|
|
96
|
+
type Key = string | number | symbol;
|
|
97
|
+
/** The raw context snapshot containing visible and hidden Map buckets */
|
|
98
|
+
type Store = {
|
|
99
|
+
hidden: Map<Key, unknown>;
|
|
100
|
+
visible: Map<Key, unknown>;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Request-scoped key-value storage backed by `AsyncLocalStorage`.
|
|
104
|
+
*
|
|
105
|
+
* Each request gets its own isolated context so data never leaks between
|
|
106
|
+
* requests. Values are split into two buckets:
|
|
107
|
+
*
|
|
108
|
+
* - **Visible** – included when calling {@link all} (safe for logging).
|
|
109
|
+
* - **Hidden** – accessible only through the `*Hidden` methods and excluded
|
|
110
|
+
* from logging, intended for sensitive data like tokens or credentials.
|
|
111
|
+
*
|
|
112
|
+
* Start a new context with {@link run}, then use the remaining methods to
|
|
113
|
+
* read and write within that scope.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* Context.run(() => {
|
|
118
|
+
* Context.add('userId', 123)
|
|
119
|
+
* Context.get<number>('userId') // 123
|
|
120
|
+
* Context.all() // { userId: 123 }
|
|
121
|
+
* })
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
interface ContextService {
|
|
125
|
+
/** Initializes a fresh context store for the duration of `callback`. */
|
|
126
|
+
run<T>(callback: () => T): T;
|
|
127
|
+
/** Stores a key-value pair in the visible context, overriding any existing value. */
|
|
128
|
+
add(key: Key, value: any): void;
|
|
129
|
+
/** Removes one or more keys from the visible context. */
|
|
130
|
+
del(...key: Key[]): void;
|
|
131
|
+
/** Appends value(s) to an array in the visible context. Creates the array if the key doesn't exist. */
|
|
132
|
+
push(key: Key, ...value: any[]): void;
|
|
133
|
+
/** Returns `true` if the key exists in the visible context. */
|
|
134
|
+
has(key: Key): boolean;
|
|
135
|
+
/** Retrieves the value for `key` from the visible context, or `undefined` if absent. */
|
|
136
|
+
get<Value = unknown>(key: Key): Value | undefined;
|
|
137
|
+
/** Returns all visible context entries as a plain object. */
|
|
138
|
+
all(): {
|
|
139
|
+
[key: Key]: unknown;
|
|
140
|
+
};
|
|
141
|
+
/** Returns convenience methods scoped to a single key in the visible context. */
|
|
142
|
+
createItem<Value = unknown>(key: Key): {
|
|
143
|
+
add(value: Value): void;
|
|
144
|
+
del(): void;
|
|
145
|
+
push: Value extends Array<any> ? (...value: Value) => void : never;
|
|
146
|
+
get(): Value | undefined;
|
|
147
|
+
};
|
|
148
|
+
/** Stores a key-value pair in the hidden context (excluded from {@link all}). */
|
|
149
|
+
addHidden(key: Key, value: any): void;
|
|
150
|
+
/** Removes one or more keys from the hidden context. */
|
|
151
|
+
delHidden(...key: Key[]): void;
|
|
152
|
+
/** Appends value(s) to an array in the hidden context. */
|
|
153
|
+
pushHidden(key: Key, ...value: any[]): void;
|
|
154
|
+
/** Returns `true` if the key exists in the hidden context. */
|
|
155
|
+
hasHidden(key: Key): boolean;
|
|
156
|
+
/** Retrieves the value for `key` from the hidden context, or `undefined` if absent. */
|
|
157
|
+
getHidden<Value = unknown>(key: Key): Value | undefined;
|
|
158
|
+
/** Returns all hidden context entries as a plain object. */
|
|
159
|
+
allHidden(): {
|
|
160
|
+
[key: Key]: unknown;
|
|
161
|
+
};
|
|
162
|
+
/** Returns convenience methods scoped to a single key in the hidden context. */
|
|
163
|
+
createHiddenItem<Value = unknown>(key: Key): {
|
|
164
|
+
add(value: Value): void;
|
|
165
|
+
del(): void;
|
|
166
|
+
push: Value extends Array<any> ? (...value: Value) => void : never;
|
|
167
|
+
get(): Value | undefined;
|
|
168
|
+
};
|
|
169
|
+
/** Runs `callback` within the given store so context is available inside. */
|
|
170
|
+
runWith<T>(store: Store | undefined, callback: () => T): T;
|
|
171
|
+
/** Returns a reference to the current AsyncLocalStorage store, or `undefined` outside a `run()`. */
|
|
172
|
+
getStore(): Store | undefined;
|
|
173
|
+
}
|
|
174
|
+
namespace Provider {
|
|
175
|
+
type Key = keyof KeysWithFallback;
|
|
176
|
+
interface Keys {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { Context };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3
|
+
import { ServiceRegistry } from "@maestro-js/service-registry";
|
|
4
|
+
function create() {
|
|
5
|
+
const asyncStore = new AsyncLocalStorage();
|
|
6
|
+
function makeContextFns(getContext) {
|
|
7
|
+
function add2(key, value) {
|
|
8
|
+
const store = getContext();
|
|
9
|
+
if (store) {
|
|
10
|
+
store.set(key, value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function del2(...key) {
|
|
14
|
+
const store = getContext();
|
|
15
|
+
if (store) {
|
|
16
|
+
key.forEach((k) => store.delete(k));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function push2(key, ...value) {
|
|
20
|
+
const store = getContext();
|
|
21
|
+
if (store) {
|
|
22
|
+
const arr = store.get(key);
|
|
23
|
+
if (arr === void 0) {
|
|
24
|
+
store.set(key, [...value]);
|
|
25
|
+
} else if (Array.isArray(arr)) {
|
|
26
|
+
store.set(key, [...arr, ...value]);
|
|
27
|
+
} else {
|
|
28
|
+
store.set(key, [arr, ...value]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function has2(key) {
|
|
33
|
+
const store = getContext();
|
|
34
|
+
return !!store?.has(key);
|
|
35
|
+
}
|
|
36
|
+
function get2(key) {
|
|
37
|
+
const store = getContext();
|
|
38
|
+
return store?.get(key);
|
|
39
|
+
}
|
|
40
|
+
function all2() {
|
|
41
|
+
const store = getContext();
|
|
42
|
+
const entries = store ? [...store.entries()] : [];
|
|
43
|
+
return Object.fromEntries(entries);
|
|
44
|
+
}
|
|
45
|
+
function createItem2(key) {
|
|
46
|
+
return {
|
|
47
|
+
add(value) {
|
|
48
|
+
add2(key, value);
|
|
49
|
+
},
|
|
50
|
+
del() {
|
|
51
|
+
del2(key);
|
|
52
|
+
},
|
|
53
|
+
push: ((...value) => {
|
|
54
|
+
push2(key, ...value);
|
|
55
|
+
}),
|
|
56
|
+
get() {
|
|
57
|
+
return get2(key);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { add: add2, del: del2, push: push2, has: has2, get: get2, all: all2, createItem: createItem2 };
|
|
62
|
+
}
|
|
63
|
+
const { add, del, push, has, get, all, createItem } = makeContextFns(() => asyncStore.getStore()?.visible);
|
|
64
|
+
const {
|
|
65
|
+
add: addHidden,
|
|
66
|
+
del: delHidden,
|
|
67
|
+
push: pushHidden,
|
|
68
|
+
has: hasHidden,
|
|
69
|
+
get: getHidden,
|
|
70
|
+
all: allHidden,
|
|
71
|
+
createItem: createHiddenItem
|
|
72
|
+
} = makeContextFns(() => asyncStore.getStore()?.hidden);
|
|
73
|
+
function run(callback) {
|
|
74
|
+
return asyncStore.run({ visible: /* @__PURE__ */ new Map(), hidden: /* @__PURE__ */ new Map() }, callback);
|
|
75
|
+
}
|
|
76
|
+
function runWith(store, callback) {
|
|
77
|
+
if (store) {
|
|
78
|
+
return asyncStore.run(store, callback);
|
|
79
|
+
} else {
|
|
80
|
+
return callback();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function getStore() {
|
|
84
|
+
return asyncStore.getStore();
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
run,
|
|
88
|
+
add,
|
|
89
|
+
del,
|
|
90
|
+
push,
|
|
91
|
+
has,
|
|
92
|
+
get,
|
|
93
|
+
all,
|
|
94
|
+
createItem,
|
|
95
|
+
addHidden,
|
|
96
|
+
delHidden,
|
|
97
|
+
pushHidden,
|
|
98
|
+
hasHidden,
|
|
99
|
+
getHidden,
|
|
100
|
+
allHidden,
|
|
101
|
+
createHiddenItem,
|
|
102
|
+
runWith,
|
|
103
|
+
getStore
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
var registry = ServiceRegistry.createRegistry(
|
|
107
|
+
ServiceRegistry.proxyFunctionsForObject
|
|
108
|
+
);
|
|
109
|
+
var Provider = {
|
|
110
|
+
create,
|
|
111
|
+
register: registry.register
|
|
112
|
+
};
|
|
113
|
+
function provider(key) {
|
|
114
|
+
const service = registry.resolve(key);
|
|
115
|
+
return {
|
|
116
|
+
run: service.run,
|
|
117
|
+
add: service.add,
|
|
118
|
+
del: service.del,
|
|
119
|
+
push: service.push,
|
|
120
|
+
has: service.has,
|
|
121
|
+
get: service.get,
|
|
122
|
+
all: service.all,
|
|
123
|
+
createItem: service.createItem,
|
|
124
|
+
addHidden: service.addHidden,
|
|
125
|
+
delHidden: service.delHidden,
|
|
126
|
+
pushHidden: service.pushHidden,
|
|
127
|
+
hasHidden: service.hasHidden,
|
|
128
|
+
getHidden: service.getHidden,
|
|
129
|
+
allHidden: service.allHidden,
|
|
130
|
+
createHiddenItem: service.createHiddenItem,
|
|
131
|
+
runWith: service.runWith,
|
|
132
|
+
getStore: service.getStore
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
Provider.register("default", create());
|
|
136
|
+
var Context = {
|
|
137
|
+
Provider,
|
|
138
|
+
...provider("default"),
|
|
139
|
+
provider
|
|
140
|
+
};
|
|
141
|
+
export {
|
|
142
|
+
Context
|
|
143
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/context",
|
|
3
|
+
"description": "Guide for working with @maestro-js/context, a request-scoped key-value storage package backed by Node.js AsyncLocalStorage. Use when working with @maestro-js/context, adding per-request state, storing visible or hidden context values, propagating context across async boundaries, integrating context with logging or tracing, creating typed context item handles, or transferring context stores between execution scopes. Covers the Provider pattern, visible/hidden buckets, run/runWith lifecycle, createItem typed handles, getStore for context propagation, and testing patterns.",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@maestro-js/service-registry": "1.0.0-alpha.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^22.9.0"
|
|
16
|
+
},
|
|
17
|
+
"version": "1.0.0-alpha.0",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "restricted"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.18.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup --config ../../tsup.config.ts",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"test": "npx beartest ./tests",
|
|
33
|
+
"format": "prettier --write src/",
|
|
34
|
+
"lint": "prettier --check src/"
|
|
35
|
+
}
|
|
36
|
+
}
|