@declaro/core 2.0.0-beta.99 → 2.1.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.
Files changed (60) hide show
  1. package/dist/browser/index.js +21 -27
  2. package/dist/browser/index.js.map +37 -27
  3. package/dist/browser/scope/index.js +1 -2
  4. package/dist/browser/scope/index.js.map +1 -1
  5. package/dist/bun/index.js +19011 -0
  6. package/dist/bun/index.js.map +132 -0
  7. package/dist/bun/scope/index.js +4 -0
  8. package/dist/bun/scope/index.js.map +9 -0
  9. package/dist/node/index.cjs +2581 -874
  10. package/dist/node/index.cjs.map +36 -27
  11. package/dist/node/index.js +2572 -868
  12. package/dist/node/index.js.map +36 -27
  13. package/dist/node/scope/index.cjs +31 -10
  14. package/dist/node/scope/index.cjs.map +1 -1
  15. package/dist/node/scope/index.js +1 -27
  16. package/dist/node/scope/index.js.map +1 -1
  17. package/dist/ts/context/async-context.d.ts +54 -0
  18. package/dist/ts/context/async-context.d.ts.map +1 -0
  19. package/dist/ts/context/async-context.test.d.ts +2 -0
  20. package/dist/ts/context/async-context.test.d.ts.map +1 -0
  21. package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
  22. package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
  23. package/dist/ts/context/context.d.ts +297 -38
  24. package/dist/ts/context/context.d.ts.map +1 -1
  25. package/dist/ts/http/request-context.d.ts.map +1 -1
  26. package/dist/ts/index.d.ts +2 -0
  27. package/dist/ts/index.d.ts.map +1 -1
  28. package/dist/ts/schema/json-schema.d.ts +9 -1
  29. package/dist/ts/schema/json-schema.d.ts.map +1 -1
  30. package/dist/ts/schema/model.d.ts +6 -1
  31. package/dist/ts/schema/model.d.ts.map +1 -1
  32. package/dist/ts/schema/test/mock-model.d.ts +2 -2
  33. package/dist/ts/schema/test/mock-model.d.ts.map +1 -1
  34. package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
  35. package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
  36. package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
  37. package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
  38. package/dist/ts/shims/async-local-storage.d.ts +36 -0
  39. package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
  40. package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
  41. package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
  42. package/package.json +17 -9
  43. package/src/context/async-context.test.ts +348 -0
  44. package/src/context/async-context.ts +129 -0
  45. package/src/context/context.circular-deps.test.ts +1047 -0
  46. package/src/context/context.test.ts +150 -0
  47. package/src/context/context.ts +493 -55
  48. package/src/http/request-context.ts +1 -3
  49. package/src/index.ts +2 -0
  50. package/src/schema/json-schema.ts +14 -1
  51. package/src/schema/model-schema.test.ts +155 -1
  52. package/src/schema/model.ts +34 -3
  53. package/src/schema/test/mock-model.ts +6 -2
  54. package/src/shared/utils/schema-utils.test.ts +33 -0
  55. package/src/shared/utils/schema-utils.ts +17 -0
  56. package/src/shims/async-local-storage.test.ts +258 -0
  57. package/src/shims/async-local-storage.ts +82 -0
  58. package/dist/ts/schema/entity-schema.test.d.ts +0 -1
  59. package/dist/ts/schema/entity-schema.test.d.ts.map +0 -1
  60. package/src/schema/entity-schema.test.ts +0 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Browser polyfill for Node's AsyncLocalStorage.
3
+ *
4
+ * ## How it works
5
+ *
6
+ * Each AsyncLocalStorage instance has a unique Symbol as its "column" ID.
7
+ * The currently active state is a single `Map<symbol, unknown>` called a
8
+ * _frame_, where each column holds the value for one ALS instance.
9
+ *
10
+ * `run()` creates a new frame (a shallow copy of the parent frame with this
11
+ * instance's value set), makes it active for the duration of `fn`, then
12
+ * restores the previous frame in a `finally` block.
13
+ *
14
+ * Multiple ALS instances are fully isolated from each other within the same
15
+ * frame: `als1.run()` only writes to its own column and does not affect any
16
+ * other instance's column.
17
+ *
18
+ * ## Browser limitation
19
+ *
20
+ * Because browsers have no native async-context hook, the frame is propagated
21
+ * only within the synchronous call stack of `fn`. Any code that runs after an
22
+ * `await` boundary cannot see the frame that was active when the `await` was
23
+ * encountered. For true async propagation in the browser, use Zone.js or the
24
+ * forthcoming `AsyncContext` TC39 API.
25
+ */
26
+ export declare class AsyncLocalStorage<T = any> {
27
+ #private;
28
+ getStore(): T | undefined;
29
+ run<R>(store: T, fn: (...args: any[]) => R, ...args: any[]): R;
30
+ exit<R>(fn: (...args: any[]) => R, ...args: any[]): R;
31
+ enterWith(store: T): void;
32
+ disable(): void;
33
+ static bind<F extends (...args: any[]) => any>(fn: F): F;
34
+ static snapshot(): <R>(fn: (...args: any[]) => R, ...args: any[]) => R;
35
+ }
36
+ //# sourceMappingURL=async-local-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-local-storage.d.ts","sourceRoot":"","sources":["../../../src/shims/async-local-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,qBAAa,iBAAiB,CAAC,CAAC,GAAG,GAAG;;IAIlC,QAAQ,IAAI,CAAC,GAAG,SAAS;IAIzB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC;IAW9D,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC;IAWrD,SAAS,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAMzB,OAAO,IAAI,IAAI;IAMf,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC;IAIxD,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC;CAGzE"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=async-local-storage.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-local-storage.test.d.ts","sourceRoot":"","sources":["../../../src/shims/async-local-storage.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declaro/core",
3
- "version": "2.0.0-beta.99",
3
+ "version": "2.1.0",
4
4
  "description": "Declarative Business Module Definition",
5
5
  "main": "dist/node/index.js",
6
6
  "module": "dist/node/index.js",
@@ -49,24 +49,32 @@
49
49
  "url": "https://github.com/emmertio/declaro/issues"
50
50
  },
51
51
  "homepage": "https://github.com/emmertio/declaro/tree/main#readme",
52
- "gitHead": "0277b5d86fbbb5b1c1f77e367005bb11551471e7",
52
+ "gitHead": "d474a72810905706fdf1011441af4deb6c6f2620",
53
53
  "files": [
54
54
  "dist/**/*",
55
55
  "src/**/*"
56
56
  ],
57
57
  "exports": {
58
- "bun": "./src/index.ts",
59
- "types": "./dist/ts/index.d.ts",
60
- "import": "./dist/node/index.js",
58
+ "import": {
59
+ "bun": "./dist/bun/index.js",
60
+ "browser": "./dist/browser/index.js",
61
+ "types": "./dist/ts/index.d.ts",
62
+ "default": "./dist/node/index.js"
63
+ },
61
64
  "require": "./dist/node/index.cjs",
62
- "browser": "./dist/browser/index.js"
65
+ "types": "./dist/ts/index.d.ts",
66
+ "default": "./dist/node/index.js"
63
67
  },
64
68
  "imports": {
65
69
  "#scope": {
66
- "types": "./dist/ts/scope/index.d.ts",
67
- "import": "./dist/node/scope/index.js",
70
+ "import": {
71
+ "bun": "./dist/bun/scope/index.js",
72
+ "browser": "./dist/browser/scope/index.js",
73
+ "types": "./dist/ts/scope/index.d.ts",
74
+ "default": "./dist/node/scope/index.js"
75
+ },
68
76
  "require": "./dist/node/scope/index.cjs",
69
- "browser": "./dist/browser/scope/index.js"
77
+ "types": "./dist/ts/scope/index.d.ts"
70
78
  }
71
79
  }
72
80
  }
@@ -0,0 +1,348 @@
1
+ import { AsyncLocalStorage as NativeALS } from 'node:async_hooks'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { AsyncLocalStorage as ShimALS } from '../shims/async-local-storage'
4
+ import {
5
+ type AsyncContextStorage,
6
+ createContextAPI,
7
+ useContext,
8
+ withContext,
9
+ } from './async-context'
10
+ import { Context } from './context'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Shared test suite — sync behavior that works for every storage implementation.
14
+ // ---------------------------------------------------------------------------
15
+ function sharedContextTests(storage: AsyncContextStorage<Context> | undefined) {
16
+ const { withContext, useContext } = createContextAPI<Context>(storage)
17
+
18
+ it('returns the fn result synchronously', () => {
19
+ const context = new Context()
20
+ const result = withContext(context, () => 42)
21
+ expect(result).toBe(42)
22
+ })
23
+
24
+ it('returns the fn result asynchronously', async () => {
25
+ const context = new Context()
26
+ const result = await withContext(context, async () => 'hello')
27
+ expect(result).toBe('hello')
28
+ })
29
+
30
+ it('useContext() returns the active context inside withContext', () => {
31
+ const context = new Context()
32
+ withContext(context, () => {
33
+ expect(useContext()).toBe(context)
34
+ })
35
+ })
36
+
37
+ it('useContext() returns null outside of withContext', () => {
38
+ expect(useContext()).toBeNull()
39
+ })
40
+
41
+ it('useContext({ strict: true }) throws outside of withContext', () => {
42
+ expect(() => useContext({ strict: true })).toThrow(
43
+ 'useContext() was called outside of an active context'
44
+ )
45
+ })
46
+
47
+ it('useContext({ strict: true }) returns context inside withContext', () => {
48
+ const context = new Context()
49
+ withContext(context, () => {
50
+ expect(useContext({ strict: true })).toBe(context)
51
+ })
52
+ })
53
+
54
+ it('nested withContext calls use the innermost context', () => {
55
+ const outer = new Context()
56
+ const inner = new Context()
57
+
58
+ withContext(outer, () => {
59
+ expect(useContext()).toBe(outer)
60
+
61
+ withContext(inner, () => {
62
+ expect(useContext()).toBe(inner)
63
+ })
64
+
65
+ expect(useContext()).toBe(outer)
66
+ })
67
+ })
68
+
69
+ it('context is not visible after withContext completes', () => {
70
+ const context = new Context()
71
+ withContext(context, () => {})
72
+ expect(useContext()).toBeNull()
73
+ })
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Async propagation suite — only for storage types with true async support
78
+ // (native AsyncLocalStorage). The synchronous browser shim cannot propagate
79
+ // context across await boundaries.
80
+ // ---------------------------------------------------------------------------
81
+ function asyncPropagationTests(storage: AsyncContextStorage<Context> | undefined) {
82
+ const { withContext, useContext } = createContextAPI<Context>(storage)
83
+
84
+ it('useContext() returns the active context inside async withContext', async () => {
85
+ const context = new Context()
86
+ await withContext(context, async () => {
87
+ await Promise.resolve()
88
+ expect(useContext()).toBe(context)
89
+ })
90
+ })
91
+
92
+ it('context propagates across async boundaries', async () => {
93
+ const context = new Context()
94
+ const results: (Context | null)[] = []
95
+
96
+ await withContext(context, async () => {
97
+ results.push(useContext())
98
+ await new Promise((resolve) => setTimeout(resolve, 0))
99
+ results.push(useContext())
100
+ })
101
+
102
+ expect(results).toEqual([context, context])
103
+ })
104
+
105
+ describe('nested async contexts (request → event fork)', () => {
106
+ it('full lifecycle: null → outer → inner → outer → null', async () => {
107
+ const requestContext = new Context()
108
+ const eventContext = new Context()
109
+ const snapshots: (Context | null)[] = []
110
+
111
+ snapshots.push(useContext())
112
+
113
+ await withContext(requestContext, async () => {
114
+ snapshots.push(useContext())
115
+
116
+ await withContext(eventContext, async () => {
117
+ snapshots.push(useContext())
118
+ })
119
+
120
+ snapshots.push(useContext())
121
+ })
122
+
123
+ snapshots.push(useContext())
124
+
125
+ expect(snapshots).toEqual([null, requestContext, eventContext, requestContext, null])
126
+ })
127
+
128
+ it('concurrent async tasks each see their own context independently', async () => {
129
+ const requestContext = new Context()
130
+ const eventContext = new Context()
131
+ const requestSnapshots: (Context | null)[] = []
132
+ const eventSnapshots: (Context | null)[] = []
133
+
134
+ await Promise.all([
135
+ withContext(requestContext, async () => {
136
+ requestSnapshots.push(useContext())
137
+ await new Promise((resolve) => setTimeout(resolve, 10))
138
+ requestSnapshots.push(useContext())
139
+ }),
140
+ withContext(eventContext, async () => {
141
+ eventSnapshots.push(useContext())
142
+ await new Promise((resolve) => setTimeout(resolve, 5))
143
+ eventSnapshots.push(useContext())
144
+ }),
145
+ ])
146
+
147
+ expect(requestSnapshots).toEqual([requestContext, requestContext])
148
+ expect(eventSnapshots).toEqual([eventContext, eventContext])
149
+ })
150
+
151
+ it('inner withContext with forked context does not bleed into outer after completion', async () => {
152
+ const requestContext = new Context()
153
+ const eventContext = new Context()
154
+
155
+ await withContext(requestContext, async () => {
156
+ const eventDone = withContext(eventContext, async () => {
157
+ await Promise.resolve()
158
+ expect(useContext()).toBe(eventContext)
159
+ })
160
+
161
+ expect(useContext()).toBe(requestContext)
162
+
163
+ await eventDone
164
+
165
+ expect(useContext()).toBe(requestContext)
166
+ })
167
+
168
+ expect(useContext()).toBeNull()
169
+ })
170
+ })
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Native AsyncLocalStorage — full async propagation
175
+ // ---------------------------------------------------------------------------
176
+ describe('withContext / useContext (native AsyncLocalStorage)', () => {
177
+ sharedContextTests(new NativeALS<Context>())
178
+ asyncPropagationTests(new NativeALS<Context>())
179
+
180
+ // These tests exercise Context.emit() which calls the module-level
181
+ // withContext (always native). They verify the framework integration and
182
+ // are only meaningful with true async propagation.
183
+ describe('typed scopes with event listeners', () => {
184
+ interface AppScope {
185
+ foo: string
186
+ bar: number
187
+ }
188
+
189
+ interface RequestScope extends AppScope {
190
+ baz: boolean
191
+ }
192
+
193
+ it("event listener receives the emitter's context, not the registration context", async () => {
194
+ const appContext = new Context<AppScope>()
195
+
196
+ let capturedViaALS: Context | null = null
197
+ let capturedViaArg: Context | null = null
198
+
199
+ appContext.on('testEvent', async (listenerContext) => {
200
+ capturedViaArg = listenerContext
201
+ capturedViaALS = useContext()
202
+ })
203
+
204
+ const requestContext = new Context<RequestScope>().extend(appContext)
205
+
206
+ await withContext(appContext, async () => {
207
+ await withContext(requestContext, async () => {
208
+ await requestContext.emit('testEvent')
209
+ })
210
+ })
211
+
212
+ expect(capturedViaArg).toBe(requestContext)
213
+ expect(capturedViaALS).toBe(requestContext)
214
+ })
215
+
216
+ it('useContext() returns the correct typed context across nested scopes', async () => {
217
+ const appContext = new Context<AppScope>()
218
+ appContext.registerValue('foo', 'hello')
219
+ appContext.registerValue('bar', 42)
220
+
221
+ const snapshots: (Context | null)[] = []
222
+
223
+ appContext.on('testEvent', async () => {
224
+ snapshots.push(useContext())
225
+ })
226
+
227
+ const requestContext = new Context<RequestScope>().extend(appContext)
228
+ requestContext.registerValue('baz', true)
229
+
230
+ await withContext(appContext, async () => {
231
+ snapshots.push(useContext())
232
+
233
+ await withContext(requestContext, async () => {
234
+ snapshots.push(useContext())
235
+ await requestContext.emit('testEvent')
236
+ })
237
+
238
+ snapshots.push(useContext())
239
+ })
240
+
241
+ expect(snapshots).toEqual([appContext, requestContext, requestContext, appContext])
242
+ })
243
+
244
+ it('useContext<RequestScope>() narrows the type inside the request scope', async () => {
245
+ const appContext = new Context<AppScope>()
246
+
247
+ let resolvedBaz: boolean | undefined
248
+
249
+ appContext.on('testEvent', async () => {
250
+ const ctx = useContext<Context<RequestScope>>()
251
+ resolvedBaz = ctx?.resolve('baz')
252
+ })
253
+
254
+ const requestContext = new Context<RequestScope>().extend(appContext)
255
+ requestContext.registerValue('baz', true)
256
+
257
+ await withContext(requestContext, async () => {
258
+ await requestContext.emit('testEvent')
259
+ })
260
+
261
+ expect(resolvedBaz).toBe(true)
262
+ })
263
+
264
+ it('strict useContext() succeeds inside a listener because emit sets the context', async () => {
265
+ const appContext = new Context<AppScope>()
266
+ let capturedContext: Context | null = null
267
+
268
+ appContext.on('testEvent', async () => {
269
+ capturedContext = useContext({ strict: true })
270
+ })
271
+
272
+ await appContext.emit('testEvent')
273
+
274
+ expect(capturedContext).toBe(appContext)
275
+ })
276
+ })
277
+ })
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Browser shim — synchronous propagation only.
281
+ // Context is available within the synchronous call stack of withContext, but
282
+ // is not propagated across await boundaries. This is expected and correct for
283
+ // browser environments where native async hooks are unavailable.
284
+ // ---------------------------------------------------------------------------
285
+ describe('withContext / useContext (browser shim AsyncLocalStorage)', () => {
286
+ sharedContextTests(new ShimALS<Context>())
287
+
288
+ const { withContext, useContext } = createContextAPI<Context>(new ShimALS<Context>())
289
+
290
+ describe('async behavior (sync-only shim)', () => {
291
+ it('context is available at the start of an async fn before any await', async () => {
292
+ const context = new Context()
293
+ let captured: Context | null = null
294
+
295
+ await withContext(context, async () => {
296
+ captured = useContext()
297
+ })
298
+
299
+ expect(captured).toBe(context)
300
+ })
301
+
302
+ it('context is not available after an await boundary (expected shim behavior)', async () => {
303
+ const context = new Context()
304
+ let captured: Context | null | 'sentinel' = 'sentinel'
305
+
306
+ await withContext(context, async () => {
307
+ await Promise.resolve()
308
+ captured = useContext()
309
+ })
310
+
311
+ expect(captured).toBeNull()
312
+ })
313
+
314
+ it('full lifecycle: null → outer → inner → null → null (sync-only)', async () => {
315
+ const requestContext = new Context()
316
+ const eventContext = new Context()
317
+ const snapshots: (Context | null)[] = []
318
+
319
+ snapshots.push(useContext())
320
+
321
+ await withContext(requestContext, async () => {
322
+ snapshots.push(useContext())
323
+
324
+ await withContext(eventContext, async () => {
325
+ snapshots.push(useContext())
326
+ })
327
+
328
+ snapshots.push(useContext())
329
+ })
330
+
331
+ snapshots.push(useContext())
332
+
333
+ // The shim restores context synchronously when run() exits.
334
+ // After awaiting the inner withContext, the outer context is gone.
335
+ expect(snapshots).toEqual([null, requestContext, eventContext, null, null])
336
+ })
337
+ })
338
+ })
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Default storage — no storage argument; createContextAPI creates its own
342
+ // AsyncLocalStorage internally. In browser builds this resolves to the shim.
343
+ // In Node/Bun (where tests run) it resolves to native AsyncLocalStorage.
344
+ // ---------------------------------------------------------------------------
345
+ describe('withContext / useContext (default storage)', () => {
346
+ sharedContextTests(undefined)
347
+ asyncPropagationTests(undefined)
348
+ })
@@ -0,0 +1,129 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import type { Context } from './context'
3
+
4
+ /**
5
+ * Minimal interface for the storage backing `withContext` / `useContext`.
6
+ * Satisfied by Node's native `AsyncLocalStorage` and by the browser shim.
7
+ */
8
+ export interface AsyncContextStorage<C extends Context = Context> {
9
+ run<R>(store: C, fn: (...args: any[]) => R, ...args: any[]): R
10
+ getStore(): C | undefined
11
+ }
12
+
13
+ export interface UseContextOptions {
14
+ strict?: boolean
15
+ }
16
+
17
+ /**
18
+ * Create a `withContext` / `useContext` pair backed by the given storage.
19
+ *
20
+ * The generic type parameter `C` fixes the context type enforced by both
21
+ * functions, keeping the pair consistent. If no storage is provided, a new
22
+ * `AsyncLocalStorage<C>` is created automatically — in browser builds this
23
+ * resolves to the synchronous shim, so callers don't need to think about it.
24
+ *
25
+ * Pass a custom storage explicitly when you need an alternative
26
+ * implementation (e.g. a test spy or the browser shim in a test environment):
27
+ *
28
+ * @example Default (auto-polyfilled in browser builds)
29
+ * ```ts
30
+ * const { withContext, useContext } = createContextAPI<MyContext>()
31
+ * ```
32
+ *
33
+ * @example Custom storage (for testing or advanced use)
34
+ * ```ts
35
+ * import { AsyncLocalStorage } from '../shims/async-local-storage'
36
+ * const { withContext, useContext } = createContextAPI<MyContext>(new AsyncLocalStorage())
37
+ * ```
38
+ */
39
+ export function createContextAPI<C extends Context = Context>(storage?: AsyncContextStorage<C>) {
40
+ const _storage: AsyncContextStorage<C> = storage ?? new AsyncLocalStorage<C>()
41
+
42
+ /**
43
+ * Run `fn` with `context` bound as the active async context. Inside `fn`
44
+ * (and any async work it triggers, including across `await` boundaries),
45
+ * calls to `useContext()` will return this context.
46
+ *
47
+ * Nesting is fully supported: an inner `withContext` creates a child scope
48
+ * that sees its own context; when it exits the parent scope is restored.
49
+ *
50
+ * Internally uses `AsyncLocalStorage` from `node:async_hooks`. In browser
51
+ * environments, a synchronous shim is substituted at build time — context
52
+ * propagates correctly across sequential async flows (event listeners,
53
+ * middleware chains) but not across concurrent async tasks running in
54
+ * parallel.
55
+ *
56
+ * @param context - The context to make active for the duration of `fn`.
57
+ * @param fn - Arbitrary sync or async callback to run inside the context.
58
+ * @returns Whatever `fn` returns.
59
+ *
60
+ * @example Basic usage
61
+ * ```ts
62
+ * await withContext(requestContext, async () => {
63
+ * await eventEmitter.emitAsync(event) // listeners can call useContext()
64
+ * })
65
+ * ```
66
+ *
67
+ * @example Forking the context for an event
68
+ * ```ts
69
+ * await withContext(requestContext, async () => {
70
+ * const eventContext = requestContext.extend()
71
+ * await withContext(eventContext, () => emitter.emitAsync(event))
72
+ * // useContext() here still returns requestContext
73
+ * })
74
+ * ```
75
+ *
76
+ * @example Creating a context-specific helper (recommended pattern)
77
+ * ```ts
78
+ * // For withContext, define a typed wrapper since TypeScript cannot partially
79
+ * // apply the return-type parameter via an instantiation expression:
80
+ * const withMyContext = <T>(context: MyContext, fn: () => T) =>
81
+ * withContext<T>(context, fn)
82
+ * ```
83
+ */
84
+ function withContext<T = unknown>(context: C, fn: () => T): T {
85
+ return _storage.run(context, fn)
86
+ }
87
+
88
+ /**
89
+ * Return the `Context` currently active in async local storage, or `null`
90
+ * if called outside any `withContext` block.
91
+ *
92
+ * The optional generic parameter `U` narrows the return type to a subclass
93
+ * of `C` when you know the active context is more specific.
94
+ *
95
+ * Backed by `AsyncLocalStorage` on Node/Bun and a synchronous shim in
96
+ * browser builds. See `withContext` for details on browser limitations.
97
+ *
98
+ * @param options.strict - When `true`, throws instead of returning `null`.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const context = useContext() // C | null
103
+ * const context = useContext({ strict: true }) // C (throws if missing)
104
+ * ```
105
+ *
106
+ * @example Narrowing to a subtype
107
+ * ```ts
108
+ * const useMyContext = useContext<MyContext>
109
+ * ```
110
+ */
111
+ function useContext<U extends C = C>(options?: { strict?: false }): U | null
112
+ function useContext<U extends C = C>(options: { strict: true }): U
113
+ function useContext<U extends C = C>(options?: UseContextOptions): U | null {
114
+ const context = (_storage.getStore() ?? null) as U | null
115
+ if (!context && options?.strict) {
116
+ throw new Error(
117
+ 'useContext() was called outside of an active context. Wrap your code with withContext().'
118
+ )
119
+ }
120
+ return context
121
+ }
122
+
123
+ return { withContext, useContext }
124
+ }
125
+
126
+ // Module-level pair backed by the default AsyncLocalStorage.
127
+ // In browser builds the import of node:async_hooks is swapped for the
128
+ // synchronous shim at build time, so this works without any extra config.
129
+ export const { withContext, useContext } = createContextAPI<Context>()