@navios/di 0.5.0 → 0.6.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 (123) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +196 -219
  3. package/docs/README.md +69 -11
  4. package/docs/api-reference.md +281 -117
  5. package/docs/container.md +220 -56
  6. package/docs/examples/request-scope-example.mts +2 -2
  7. package/docs/factory.md +3 -8
  8. package/docs/getting-started.md +37 -8
  9. package/docs/migration.md +318 -37
  10. package/docs/request-contexts.md +263 -175
  11. package/docs/scopes.md +79 -42
  12. package/lib/browser/index.d.mts +1577 -0
  13. package/lib/browser/index.d.mts.map +1 -0
  14. package/lib/browser/index.mjs +3012 -0
  15. package/lib/browser/index.mjs.map +1 -0
  16. package/lib/index-S_qX2VLI.d.mts +1211 -0
  17. package/lib/index-S_qX2VLI.d.mts.map +1 -0
  18. package/lib/index-fKPuT65j.d.cts +1206 -0
  19. package/lib/index-fKPuT65j.d.cts.map +1 -0
  20. package/lib/index.cjs +389 -0
  21. package/lib/index.cjs.map +1 -0
  22. package/lib/index.d.cts +376 -0
  23. package/lib/index.d.cts.map +1 -0
  24. package/lib/index.d.mts +371 -78
  25. package/lib/index.d.mts.map +1 -0
  26. package/lib/index.mjs +325 -63
  27. package/lib/index.mjs.map +1 -1
  28. package/lib/testing/index.cjs +9 -0
  29. package/lib/testing/index.d.cts +2 -0
  30. package/lib/testing/index.d.mts +2 -2
  31. package/lib/testing/index.mjs +2 -72
  32. package/lib/testing-BMGmmxH7.cjs +2895 -0
  33. package/lib/testing-BMGmmxH7.cjs.map +1 -0
  34. package/lib/testing-DCXz8AJD.mjs +2655 -0
  35. package/lib/testing-DCXz8AJD.mjs.map +1 -0
  36. package/package.json +26 -4
  37. package/project.json +2 -2
  38. package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
  39. package/src/__tests__/async-local-storage.spec.mts +333 -0
  40. package/src/__tests__/container.spec.mts +30 -25
  41. package/src/__tests__/e2e.browser.spec.mts +790 -0
  42. package/src/__tests__/e2e.spec.mts +1222 -0
  43. package/src/__tests__/errors.spec.mts +6 -6
  44. package/src/__tests__/factory.spec.mts +1 -1
  45. package/src/__tests__/get-injectors.spec.mts +1 -1
  46. package/src/__tests__/injectable.spec.mts +1 -1
  47. package/src/__tests__/injection-token.spec.mts +1 -1
  48. package/src/__tests__/library-findings.spec.mts +563 -0
  49. package/src/__tests__/registry.spec.mts +2 -2
  50. package/src/__tests__/request-scope.spec.mts +266 -274
  51. package/src/__tests__/service-instantiator.spec.mts +19 -17
  52. package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
  53. package/src/__tests__/service-locator-manager.spec.mts +15 -15
  54. package/src/__tests__/service-locator.spec.mts +167 -244
  55. package/src/__tests__/unified-api.spec.mts +27 -27
  56. package/src/__type-tests__/factory.spec-d.mts +2 -2
  57. package/src/__type-tests__/inject.spec-d.mts +2 -2
  58. package/src/__type-tests__/injectable.spec-d.mts +1 -1
  59. package/src/browser.mts +16 -0
  60. package/src/container/container.mts +319 -0
  61. package/src/container/index.mts +2 -0
  62. package/src/container/scoped-container.mts +350 -0
  63. package/src/decorators/factory.decorator.mts +4 -4
  64. package/src/decorators/injectable.decorator.mts +5 -5
  65. package/src/errors/di-error.mts +13 -7
  66. package/src/errors/index.mts +0 -8
  67. package/src/index.mts +156 -15
  68. package/src/interfaces/container.interface.mts +82 -0
  69. package/src/interfaces/factory.interface.mts +2 -2
  70. package/src/interfaces/index.mts +1 -0
  71. package/src/internal/context/async-local-storage.mts +120 -0
  72. package/src/internal/context/factory-context.mts +18 -0
  73. package/src/internal/context/index.mts +3 -0
  74. package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
  75. package/src/internal/context/resolution-context.mts +63 -0
  76. package/src/internal/context/sync-local-storage.mts +51 -0
  77. package/src/internal/core/index.mts +5 -0
  78. package/src/internal/core/instance-resolver.mts +641 -0
  79. package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
  80. package/src/internal/core/invalidator.mts +437 -0
  81. package/src/internal/core/service-locator.mts +202 -0
  82. package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
  83. package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
  84. package/src/internal/holder/holder-manager.mts +85 -0
  85. package/src/internal/holder/holder-storage.interface.mts +116 -0
  86. package/src/internal/holder/index.mts +6 -0
  87. package/src/internal/holder/instance-holder.mts +109 -0
  88. package/src/internal/holder/request-storage.mts +134 -0
  89. package/src/internal/holder/singleton-storage.mts +105 -0
  90. package/src/internal/index.mts +4 -0
  91. package/src/internal/lifecycle/circular-detector.mts +77 -0
  92. package/src/internal/lifecycle/index.mts +2 -0
  93. package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +12 -5
  94. package/src/testing/__tests__/test-container.spec.mts +2 -2
  95. package/src/testing/test-container.mts +4 -4
  96. package/src/token/index.mts +2 -0
  97. package/src/{injection-token.mts → token/injection-token.mts} +1 -1
  98. package/src/{registry.mts → token/registry.mts} +1 -1
  99. package/src/utils/get-injectable-token.mts +1 -1
  100. package/src/utils/get-injectors.mts +32 -15
  101. package/src/utils/types.mts +1 -1
  102. package/tsdown.config.mts +67 -0
  103. package/lib/_tsup-dts-rollup.d.mts +0 -1283
  104. package/lib/_tsup-dts-rollup.d.ts +0 -1283
  105. package/lib/chunk-44F3LXW5.mjs +0 -2043
  106. package/lib/chunk-44F3LXW5.mjs.map +0 -1
  107. package/lib/index.d.ts +0 -78
  108. package/lib/index.js +0 -2127
  109. package/lib/index.js.map +0 -1
  110. package/lib/testing/index.d.ts +0 -2
  111. package/lib/testing/index.js +0 -2060
  112. package/lib/testing/index.js.map +0 -1
  113. package/lib/testing/index.mjs.map +0 -1
  114. package/src/container.mts +0 -227
  115. package/src/factory-context.mts +0 -8
  116. package/src/instance-resolver.mts +0 -559
  117. package/src/request-context-manager.mts +0 -149
  118. package/src/service-invalidator.mts +0 -429
  119. package/src/service-locator-instance-holder.mts +0 -70
  120. package/src/service-locator-manager.mts +0 -85
  121. package/src/service-locator.mts +0 -246
  122. package/tsup.config.mts +0 -12
  123. /package/src/{injector.mts → injectors.mts} +0 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Browser environment tests for AsyncLocalStorage.
3
+ *
4
+ * These tests verify that:
5
+ * 1. The SyncLocalStorage polyfill works correctly
6
+ * 2. All DI functionality works correctly with the polyfill
7
+ * 3. The resolution context pattern works for synchronous operations
8
+ *
9
+ * Note: We use __testing__.forceSyncMode() to simulate browser behavior
10
+ * since happy-dom still runs on Node.js and has process.versions.node.
11
+ */
12
+
13
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
14
+
15
+ import {
16
+ createAsyncLocalStorage,
17
+ isUsingNativeAsyncLocalStorage,
18
+ __testing__,
19
+ } from '../internal/context/async-local-storage.mjs'
20
+ import {
21
+ getCurrentResolutionContext,
22
+ withResolutionContext,
23
+ withoutResolutionContext,
24
+ } from '../internal/context/resolution-context.mjs'
25
+ import type { InstanceHolder } from '../internal/holder/instance-holder.mjs'
26
+ import { InstanceStatus } from '../internal/holder/instance-holder.mjs'
27
+ import { InjectableScope } from '../enums/index.mjs'
28
+
29
+ // Force sync mode for all tests in this file to simulate browser behavior
30
+ beforeAll(() => {
31
+ __testing__.forceSyncMode()
32
+ })
33
+
34
+ afterAll(() => {
35
+ __testing__.reset()
36
+ })
37
+
38
+ // ============================================================================
39
+ // SECTION 1: Environment Detection
40
+ // ============================================================================
41
+
42
+ describe('Browser Environment Detection (forced sync mode)', () => {
43
+ it('should use sync polyfill when forced', () => {
44
+ // After forcing sync mode, native AsyncLocalStorage should not be used
45
+ expect(isUsingNativeAsyncLocalStorage()).toBe(false)
46
+ })
47
+
48
+ it('should create a working storage instance', () => {
49
+ const storage = createAsyncLocalStorage<{ value: number }>()
50
+ expect(storage).toBeDefined()
51
+ expect(typeof storage.run).toBe('function')
52
+ expect(typeof storage.getStore).toBe('function')
53
+ })
54
+ })
55
+
56
+ // ============================================================================
57
+ // SECTION 2: SyncLocalStorage Behavior in Browser
58
+ // ============================================================================
59
+
60
+ describe('SyncLocalStorage in Browser', () => {
61
+ it('should store and retrieve context synchronously', () => {
62
+ const storage = createAsyncLocalStorage<{ name: string }>()
63
+
64
+ expect(storage.getStore()).toBeUndefined()
65
+
66
+ storage.run({ name: 'test' }, () => {
67
+ expect(storage.getStore()?.name).toBe('test')
68
+ })
69
+
70
+ expect(storage.getStore()).toBeUndefined()
71
+ })
72
+
73
+ it('should handle nested contexts', () => {
74
+ const storage = createAsyncLocalStorage<{ depth: number }>()
75
+
76
+ storage.run({ depth: 1 }, () => {
77
+ expect(storage.getStore()?.depth).toBe(1)
78
+
79
+ storage.run({ depth: 2 }, () => {
80
+ expect(storage.getStore()?.depth).toBe(2)
81
+
82
+ storage.run({ depth: 3 }, () => {
83
+ expect(storage.getStore()?.depth).toBe(3)
84
+ })
85
+
86
+ expect(storage.getStore()?.depth).toBe(2)
87
+ })
88
+
89
+ expect(storage.getStore()?.depth).toBe(1)
90
+ })
91
+
92
+ expect(storage.getStore()).toBeUndefined()
93
+ })
94
+
95
+ it('should restore context after exceptions', () => {
96
+ const storage = createAsyncLocalStorage<{ value: number }>()
97
+
98
+ storage.run({ value: 1 }, () => {
99
+ try {
100
+ storage.run({ value: 2 }, () => {
101
+ throw new Error('test error')
102
+ })
103
+ } catch {
104
+ // Expected
105
+ }
106
+
107
+ // Should be back to outer context
108
+ expect(storage.getStore()?.value).toBe(1)
109
+ })
110
+ })
111
+ })
112
+
113
+ // ============================================================================
114
+ // SECTION 3: Resolution Context Integration
115
+ // ============================================================================
116
+
117
+ describe('Resolution Context in Browser', () => {
118
+ function createMockHolder(name: string): InstanceHolder {
119
+ return {
120
+ status: InstanceStatus.Creating,
121
+ name,
122
+ instance: null,
123
+ creationPromise: null,
124
+ destroyPromise: null,
125
+ type: class {} as unknown as InstanceHolder['type'],
126
+ scope: InjectableScope.Singleton,
127
+ deps: new Set(),
128
+ destroyListeners: [],
129
+ createdAt: Date.now(),
130
+ waitingFor: new Set(),
131
+ }
132
+ }
133
+
134
+ it('should track resolution context correctly', () => {
135
+ const holderA = createMockHolder('ServiceA')
136
+ const getHolder = () => undefined
137
+
138
+ expect(getCurrentResolutionContext()).toBeUndefined()
139
+
140
+ withResolutionContext(holderA, getHolder, () => {
141
+ const ctx = getCurrentResolutionContext()
142
+ expect(ctx).toBeDefined()
143
+ expect(ctx?.waiterHolder).toBe(holderA)
144
+ })
145
+
146
+ expect(getCurrentResolutionContext()).toBeUndefined()
147
+ })
148
+
149
+ it('should handle nested resolution contexts', () => {
150
+ const holderA = createMockHolder('ServiceA')
151
+ const holderB = createMockHolder('ServiceB')
152
+ const getHolder = () => undefined
153
+
154
+ withResolutionContext(holderA, getHolder, () => {
155
+ expect(getCurrentResolutionContext()?.waiterHolder.name).toBe('ServiceA')
156
+
157
+ withResolutionContext(holderB, getHolder, () => {
158
+ expect(getCurrentResolutionContext()?.waiterHolder.name).toBe(
159
+ 'ServiceB',
160
+ )
161
+ })
162
+
163
+ expect(getCurrentResolutionContext()?.waiterHolder.name).toBe('ServiceA')
164
+ })
165
+ })
166
+
167
+ it('should clear context with withoutResolutionContext', () => {
168
+ const holderA = createMockHolder('ServiceA')
169
+ const getHolder = () => undefined
170
+
171
+ withResolutionContext(holderA, getHolder, () => {
172
+ expect(getCurrentResolutionContext()).toBeDefined()
173
+
174
+ withoutResolutionContext(() => {
175
+ expect(getCurrentResolutionContext()).toBeUndefined()
176
+ })
177
+
178
+ expect(getCurrentResolutionContext()).toBeDefined()
179
+ })
180
+ })
181
+ })
182
+
183
+ // ============================================================================
184
+ // SECTION 4: Async Limitations in Browser
185
+ // ============================================================================
186
+
187
+ describe('Async Limitations in Browser (expected behavior)', () => {
188
+ it('should NOT propagate context across microtasks', async () => {
189
+ const storage = createAsyncLocalStorage<{ value: number }>()
190
+
191
+ let valueInMicrotask: number | undefined
192
+
193
+ storage.run({ value: 42 }, () => {
194
+ // Queue a microtask
195
+ Promise.resolve().then(() => {
196
+ // In browser with SyncLocalStorage, context is lost
197
+ valueInMicrotask = storage.getStore()?.value
198
+ })
199
+ })
200
+
201
+ // Wait for microtask to complete
202
+ await Promise.resolve()
203
+ await Promise.resolve()
204
+
205
+ // Context should be undefined because SyncLocalStorage doesn't propagate
206
+ expect(valueInMicrotask).toBeUndefined()
207
+ })
208
+
209
+ it('should NOT propagate context across setTimeout', async () => {
210
+ const storage = createAsyncLocalStorage<{ value: number }>()
211
+
212
+ const valuePromise = new Promise<number | undefined>((resolve) => {
213
+ storage.run({ value: 42 }, () => {
214
+ setTimeout(() => {
215
+ resolve(storage.getStore()?.value)
216
+ }, 0)
217
+ })
218
+ })
219
+
220
+ const value = await valuePromise
221
+ // Context should be undefined
222
+ expect(value).toBeUndefined()
223
+ })
224
+
225
+ it('should still work for synchronous code in async functions', () => {
226
+ const storage = createAsyncLocalStorage<{ value: number }>()
227
+
228
+ // This is the key behavior: even in browser, synchronous access works
229
+ storage.run({ value: 42 }, () => {
230
+ // Synchronous access within the run callback works
231
+ expect(storage.getStore()?.value).toBe(42)
232
+
233
+ // Nested synchronous calls work
234
+ function nestedCall() {
235
+ return storage.getStore()?.value
236
+ }
237
+ expect(nestedCall()).toBe(42)
238
+ })
239
+ })
240
+ })
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Tests for the cross-platform AsyncLocalStorage implementation.
3
+ *
4
+ * These tests verify:
5
+ * 1. SyncLocalStorage polyfill works correctly for synchronous code
6
+ * 2. The runtime detection correctly identifies the environment
7
+ * 3. Both implementations provide consistent behavior for sync use cases
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest'
11
+
12
+ import { SyncLocalStorage } from '../internal/context/sync-local-storage.mjs'
13
+ import {
14
+ createAsyncLocalStorage,
15
+ isUsingNativeAsyncLocalStorage,
16
+ } from '../internal/context/async-local-storage.mjs'
17
+
18
+ // ============================================================================
19
+ // SECTION 1: SyncLocalStorage Unit Tests
20
+ // ============================================================================
21
+
22
+ describe('SyncLocalStorage', () => {
23
+ it('should return undefined when no context is active', () => {
24
+ const storage = new SyncLocalStorage<{ value: number }>()
25
+ expect(storage.getStore()).toBeUndefined()
26
+ })
27
+
28
+ it('should return the store within a run() call', () => {
29
+ const storage = new SyncLocalStorage<{ value: number }>()
30
+ const store = { value: 42 }
31
+
32
+ storage.run(store, () => {
33
+ expect(storage.getStore()).toBe(store)
34
+ })
35
+ })
36
+
37
+ it('should return undefined after run() completes', () => {
38
+ const storage = new SyncLocalStorage<{ value: number }>()
39
+ const store = { value: 42 }
40
+
41
+ storage.run(store, () => {
42
+ // Inside context
43
+ })
44
+
45
+ expect(storage.getStore()).toBeUndefined()
46
+ })
47
+
48
+ it('should handle nested run() calls correctly', () => {
49
+ const storage = new SyncLocalStorage<{ value: number }>()
50
+ const outer = { value: 1 }
51
+ const inner = { value: 2 }
52
+
53
+ storage.run(outer, () => {
54
+ expect(storage.getStore()).toBe(outer)
55
+
56
+ storage.run(inner, () => {
57
+ expect(storage.getStore()).toBe(inner)
58
+ })
59
+
60
+ // After inner run completes, should be back to outer
61
+ expect(storage.getStore()).toBe(outer)
62
+ })
63
+
64
+ // After all runs complete
65
+ expect(storage.getStore()).toBeUndefined()
66
+ })
67
+
68
+ it('should handle deeply nested run() calls', () => {
69
+ const storage = new SyncLocalStorage<{ depth: number }>()
70
+ const stores = Array.from({ length: 5 }, (_, i) => ({ depth: i }))
71
+
72
+ function nest(depth: number): void {
73
+ if (depth >= stores.length) return
74
+
75
+ storage.run(stores[depth], () => {
76
+ expect(storage.getStore()).toBe(stores[depth])
77
+ nest(depth + 1)
78
+ expect(storage.getStore()).toBe(stores[depth])
79
+ })
80
+ }
81
+
82
+ nest(0)
83
+ expect(storage.getStore()).toBeUndefined()
84
+ })
85
+
86
+ it('should return the value from run()', () => {
87
+ const storage = new SyncLocalStorage<{ value: number }>()
88
+ const store = { value: 42 }
89
+
90
+ const result = storage.run(store, () => {
91
+ return 'hello'
92
+ })
93
+
94
+ expect(result).toBe('hello')
95
+ })
96
+
97
+ it('should propagate errors and still restore context', () => {
98
+ const storage = new SyncLocalStorage<{ value: number }>()
99
+ const store = { value: 42 }
100
+
101
+ expect(() => {
102
+ storage.run(store, () => {
103
+ throw new Error('test error')
104
+ })
105
+ }).toThrow('test error')
106
+
107
+ // Context should be restored even after error
108
+ expect(storage.getStore()).toBeUndefined()
109
+ })
110
+
111
+ it('should restore context after error in nested run()', () => {
112
+ const storage = new SyncLocalStorage<{ value: number }>()
113
+ const outer = { value: 1 }
114
+ const inner = { value: 2 }
115
+
116
+ storage.run(outer, () => {
117
+ try {
118
+ storage.run(inner, () => {
119
+ throw new Error('inner error')
120
+ })
121
+ } catch {
122
+ // Catch the error
123
+ }
124
+
125
+ // Should be back to outer context
126
+ expect(storage.getStore()).toBe(outer)
127
+ })
128
+ })
129
+
130
+ it('exit() should clear the context temporarily', () => {
131
+ const storage = new SyncLocalStorage<{ value: number }>()
132
+ const store = { value: 42 }
133
+
134
+ storage.run(store, () => {
135
+ expect(storage.getStore()).toBe(store)
136
+
137
+ storage.exit(() => {
138
+ expect(storage.getStore()).toBeUndefined()
139
+ })
140
+
141
+ // After exit, should be back to the original context
142
+ expect(storage.getStore()).toBe(store)
143
+ })
144
+ })
145
+
146
+ it('exit() should restore context after error', () => {
147
+ const storage = new SyncLocalStorage<{ value: number }>()
148
+ const store = { value: 42 }
149
+
150
+ storage.run(store, () => {
151
+ try {
152
+ storage.exit(() => {
153
+ throw new Error('exit error')
154
+ })
155
+ } catch {
156
+ // Catch the error
157
+ }
158
+
159
+ // Context should be restored
160
+ expect(storage.getStore()).toBe(store)
161
+ })
162
+ })
163
+ })
164
+
165
+ // ============================================================================
166
+ // SECTION 2: createAsyncLocalStorage Tests
167
+ // ============================================================================
168
+
169
+ describe('createAsyncLocalStorage', () => {
170
+ it('should create a storage instance', () => {
171
+ const storage = createAsyncLocalStorage<{ value: number }>()
172
+ expect(storage).toBeDefined()
173
+ expect(typeof storage.run).toBe('function')
174
+ expect(typeof storage.getStore).toBe('function')
175
+ })
176
+
177
+ it('should work with run() and getStore()', () => {
178
+ const storage = createAsyncLocalStorage<{ value: number }>()
179
+ const store = { value: 42 }
180
+
181
+ storage.run(store, () => {
182
+ expect(storage.getStore()).toBe(store)
183
+ })
184
+
185
+ expect(storage.getStore()).toBeUndefined()
186
+ })
187
+
188
+ it('should handle nested contexts', () => {
189
+ const storage = createAsyncLocalStorage<{ name: string }>()
190
+
191
+ storage.run({ name: 'outer' }, () => {
192
+ expect(storage.getStore()?.name).toBe('outer')
193
+
194
+ storage.run({ name: 'inner' }, () => {
195
+ expect(storage.getStore()?.name).toBe('inner')
196
+ })
197
+
198
+ expect(storage.getStore()?.name).toBe('outer')
199
+ })
200
+ })
201
+ })
202
+
203
+ // ============================================================================
204
+ // SECTION 3: Environment Detection Tests
205
+ // ============================================================================
206
+
207
+ describe('isUsingNativeAsyncLocalStorage', () => {
208
+ it('should return a boolean', () => {
209
+ const result = isUsingNativeAsyncLocalStorage()
210
+ expect(typeof result).toBe('boolean')
211
+ })
212
+
213
+ it('should return true in Node.js environment', () => {
214
+ // Since we're running in Node.js (vitest), this should be true
215
+ expect(isUsingNativeAsyncLocalStorage()).toBe(true)
216
+ })
217
+ })
218
+
219
+ // ============================================================================
220
+ // SECTION 4: Integration with resolution context pattern
221
+ // ============================================================================
222
+
223
+ describe('Resolution Context Pattern', () => {
224
+ interface ResolutionContextData {
225
+ waiterName: string
226
+ depth: number
227
+ }
228
+
229
+ it('should work with the resolution context pattern', () => {
230
+ const resolutionContext = createAsyncLocalStorage<ResolutionContextData>()
231
+
232
+ function withResolutionContext<T>(
233
+ waiterName: string,
234
+ depth: number,
235
+ fn: () => T,
236
+ ): T {
237
+ return resolutionContext.run({ waiterName, depth }, fn)
238
+ }
239
+
240
+ function getCurrentContext(): ResolutionContextData | undefined {
241
+ return resolutionContext.getStore()
242
+ }
243
+
244
+ // Simulate nested service resolution
245
+ withResolutionContext('ServiceA', 0, () => {
246
+ const ctxA = getCurrentContext()
247
+ expect(ctxA?.waiterName).toBe('ServiceA')
248
+ expect(ctxA?.depth).toBe(0)
249
+
250
+ withResolutionContext('ServiceB', 1, () => {
251
+ const ctxB = getCurrentContext()
252
+ expect(ctxB?.waiterName).toBe('ServiceB')
253
+ expect(ctxB?.depth).toBe(1)
254
+ })
255
+
256
+ // Back to ServiceA context
257
+ const ctxAfter = getCurrentContext()
258
+ expect(ctxAfter?.waiterName).toBe('ServiceA')
259
+ })
260
+
261
+ // Outside all contexts
262
+ expect(getCurrentContext()).toBeUndefined()
263
+ })
264
+
265
+ it('should handle withoutResolutionContext pattern', () => {
266
+ const resolutionContext = createAsyncLocalStorage<ResolutionContextData>()
267
+
268
+ function withoutResolutionContext<T>(fn: () => T): T {
269
+ return resolutionContext.run(undefined as any, fn)
270
+ }
271
+
272
+ resolutionContext.run({ waiterName: 'Service', depth: 0 }, () => {
273
+ expect(resolutionContext.getStore()?.waiterName).toBe('Service')
274
+
275
+ withoutResolutionContext(() => {
276
+ // Context should be cleared (undefined-ish)
277
+ const store = resolutionContext.getStore()
278
+ expect(store === undefined || store === null).toBe(true)
279
+ })
280
+
281
+ // Context should be restored
282
+ expect(resolutionContext.getStore()?.waiterName).toBe('Service')
283
+ })
284
+ })
285
+ })
286
+
287
+ // ============================================================================
288
+ // SECTION 5: Async behavior tests (Node.js native only)
289
+ // ============================================================================
290
+
291
+ describe('Async behavior (native AsyncLocalStorage)', () => {
292
+ it('should propagate context through async/await in Node.js', async () => {
293
+ // Skip if not using native (though in Node.js vitest, it should be native)
294
+ if (!isUsingNativeAsyncLocalStorage()) {
295
+ return
296
+ }
297
+
298
+ const storage = createAsyncLocalStorage<{ value: number }>()
299
+ const store = { value: 42 }
300
+
301
+ await storage.run(store, async () => {
302
+ // Sync check
303
+ expect(storage.getStore()).toBe(store)
304
+
305
+ // After await
306
+ await Promise.resolve()
307
+ expect(storage.getStore()).toBe(store)
308
+
309
+ // After setTimeout wrapped in promise
310
+ await new Promise((resolve) => setTimeout(resolve, 10))
311
+ expect(storage.getStore()).toBe(store)
312
+ })
313
+ })
314
+
315
+ it('should propagate context through Promise.all in Node.js', async () => {
316
+ if (!isUsingNativeAsyncLocalStorage()) {
317
+ return
318
+ }
319
+
320
+ const storage = createAsyncLocalStorage<{ value: number }>()
321
+ const store = { value: 42 }
322
+
323
+ await storage.run(store, async () => {
324
+ const results = await Promise.all([
325
+ Promise.resolve().then(() => storage.getStore()?.value),
326
+ Promise.resolve().then(() => storage.getStore()?.value),
327
+ Promise.resolve().then(() => storage.getStore()?.value),
328
+ ])
329
+
330
+ expect(results).toEqual([42, 42, 42])
331
+ })
332
+ })
333
+ })