@navios/di 0.6.1 → 0.7.1

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 (50) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/lib/browser/index.d.mts +15 -84
  3. package/lib/browser/index.d.mts.map +1 -1
  4. package/lib/browser/index.mjs +121 -484
  5. package/lib/browser/index.mjs.map +1 -1
  6. package/lib/{index-DW3K5sOX.d.cts → container-BuAutHGg.d.mts} +17 -62
  7. package/lib/container-BuAutHGg.d.mts.map +1 -0
  8. package/lib/{testing-DIaIRiJz.cjs → container-DnzgpfBe.cjs} +81 -459
  9. package/lib/container-DnzgpfBe.cjs.map +1 -0
  10. package/lib/{testing-BG_fa9TJ.mjs → container-Pb_Y4Z4x.mjs} +81 -446
  11. package/lib/container-Pb_Y4Z4x.mjs.map +1 -0
  12. package/lib/{index-7jfWsiG4.d.mts → container-oGTgX2iX.d.cts} +12 -67
  13. package/lib/container-oGTgX2iX.d.cts.map +1 -0
  14. package/lib/index.cjs +44 -52
  15. package/lib/index.cjs.map +1 -1
  16. package/lib/index.d.cts +6 -25
  17. package/lib/index.d.cts.map +1 -1
  18. package/lib/index.d.mts +6 -25
  19. package/lib/index.d.mts.map +1 -1
  20. package/lib/index.mjs +2 -2
  21. package/lib/testing/index.cjs +343 -4
  22. package/lib/testing/index.cjs.map +1 -0
  23. package/lib/testing/index.d.cts +65 -2
  24. package/lib/testing/index.d.cts.map +1 -0
  25. package/lib/testing/index.d.mts +65 -2
  26. package/lib/testing/index.d.mts.map +1 -0
  27. package/lib/testing/index.mjs +341 -2
  28. package/lib/testing/index.mjs.map +1 -0
  29. package/package.json +1 -1
  30. package/src/__tests__/async-local-storage.browser.spec.mts +18 -92
  31. package/src/__tests__/container.spec.mts +93 -0
  32. package/src/__tests__/e2e.browser.spec.mts +7 -15
  33. package/src/__tests__/library-findings.spec.mts +23 -21
  34. package/src/browser.mts +4 -9
  35. package/src/container/scoped-container.mts +14 -8
  36. package/src/index.mts +5 -8
  37. package/src/internal/context/async-local-storage.browser.mts +19 -0
  38. package/src/internal/context/async-local-storage.mts +46 -98
  39. package/src/internal/context/async-local-storage.types.mts +7 -0
  40. package/src/internal/context/resolution-context.mts +23 -5
  41. package/src/internal/context/sync-local-storage.mts +3 -1
  42. package/src/internal/core/instance-resolver.mts +8 -1
  43. package/src/internal/lifecycle/circular-detector.mts +15 -0
  44. package/src/token/registry.mts +21 -0
  45. package/tsdown.config.mts +12 -1
  46. package/vitest.config.mts +25 -3
  47. package/lib/index-7jfWsiG4.d.mts.map +0 -1
  48. package/lib/index-DW3K5sOX.d.cts.map +0 -1
  49. package/lib/testing-BG_fa9TJ.mjs.map +0 -1
  50. package/lib/testing-DIaIRiJz.cjs.map +0 -1
@@ -10,10 +10,11 @@
10
10
 
11
11
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
12
12
 
13
+ import type { OnServiceDestroy } from '../interfaces/on-service-destroy.interface.mjs'
14
+
13
15
  import { Container } from '../container/container.mjs'
14
16
  import { Injectable } from '../decorators/injectable.decorator.mjs'
15
17
  import { InjectableScope } from '../enums/index.mjs'
16
- import type { OnServiceDestroy } from '../interfaces/on-service-destroy.interface.mjs'
17
18
  import { Registry } from '../token/registry.mjs'
18
19
  import { getInjectors } from '../utils/get-injectors.mjs'
19
20
 
@@ -244,19 +245,17 @@ describe('FINDING #3: Error Recovery', () => {
244
245
  })
245
246
 
246
247
  /**
247
- * DOCUMENTED BEHAVIOR: Container caches constructor errors
248
+ * DOCUMENTED BEHAVIOR: Container allows retry after constructor errors
248
249
  *
249
250
  * When a service constructor throws on first attempt, the container
250
- * caches the error state and re-throws the same error on subsequent
251
- * attempts. It does NOT retry the constructor.
251
+ * removes the failed holder from storage. This allows subsequent
252
+ * attempts to retry creating the service.
252
253
  *
253
- * This is important to understand for services that might fail due to
254
- * transient errors (network issues, resource unavailability, etc.).
255
- *
256
- * IMPLICATION: If you need retry logic for transient failures, implement
257
- * it inside the service (e.g., in onServiceInit) or use a factory pattern.
254
+ * This is useful for services that might fail due to transient errors
255
+ * (network issues, resource unavailability, etc.) - they can be
256
+ * successfully created on retry once the transient issue is resolved.
258
257
  */
259
- it('caches constructor errors and re-throws on retry (documented behavior)', async () => {
258
+ it('allows retry after constructor errors (documented behavior)', async () => {
260
259
  let attemptCount = 0
261
260
  const shouldFail = { value: true }
262
261
 
@@ -283,12 +282,11 @@ describe('FINDING #3: Error Recovery', () => {
283
282
  // Allow success on retry
284
283
  shouldFail.value = false
285
284
 
286
- // Second attempt - container caches the error and re-throws
287
- // The constructor is NOT called again
288
- await expect(container.get(FlakeyService)).rejects.toThrow(
289
- 'Transient failure',
290
- )
291
- expect(attemptCount).toBe(1) // Still 1, constructor was not retried
285
+ // Second attempt - container allows retry by removing the error holder
286
+ // The constructor IS called again
287
+ const instance = await container.get(FlakeyService)
288
+ expect(instance.getValue()).toBe('success')
289
+ expect(attemptCount).toBe(2) // Constructor was retried
292
290
  })
293
291
 
294
292
  /**
@@ -502,15 +500,19 @@ describe('FINDING #5: Cross-Storage Dependency Invalidation', () => {
502
500
 
503
501
  // Check that the singleton's holder has the request service in deps
504
502
  const manager = container.getServiceLocator().getManager()
505
- const singletonHolders = Array.from(manager.filter((h) => h.scope === InjectableScope.Singleton).values())
503
+ const singletonHolders = Array.from(
504
+ manager.filter((h) => h.scope === InjectableScope.Singleton).values(),
505
+ )
506
506
 
507
507
  // Find the SingletonWithDep holder
508
- const singletonHolder = singletonHolders.find(h => h.name.includes('SingletonWithDep'))
508
+ const singletonHolder = singletonHolders.find((h) =>
509
+ h.name.includes('SingletonWithDep'),
510
+ )
509
511
 
510
512
  if (singletonHolder) {
511
513
  // The deps should contain the RequestService instance name
512
- const hasRequestDep = Array.from(singletonHolder.deps).some(dep =>
513
- dep.includes('RequestService')
514
+ const hasRequestDep = Array.from(singletonHolder.deps).some((dep) =>
515
+ dep.includes('RequestService'),
514
516
  )
515
517
  expect(hasRequestDep).toBe(true)
516
518
  }
@@ -547,7 +549,7 @@ describe('FINDING #5: Cross-Storage Dependency Invalidation', () => {
547
549
  * invalidated when the request ends
548
550
  *
549
551
  * EDGE CASES (documented behavior):
550
- * 3. Error recovery behavior - constructor errors are cached
552
+ * 3. Error recovery behavior - constructor errors are cached - FIXED
551
553
  * - Priority: Low
552
554
  * - Impact: May prevent retry after transient failures
553
555
  * - Documented: This is intentional, use onServiceInit for retry logic
package/src/browser.mts CHANGED
@@ -1,16 +1,11 @@
1
1
  /**
2
2
  * Browser-specific entry point for @navios/di.
3
3
  *
4
- * This entry point forces the use of SyncLocalStorage instead of
5
- * Node's AsyncLocalStorage, making it safe for browser environments.
6
- *
7
- * The browser build is automatically selected by bundlers that respect
4
+ * This entry point is automatically selected by bundlers that respect
8
5
  * the "browser" condition in package.json exports.
6
+ *
7
+ * The browser build uses SyncLocalStorage instead of Node's AsyncLocalStorage,
8
+ * which is sufficient for synchronous DI resolution in browser environments.
9
9
  */
10
10
 
11
- // Force sync mode before any other imports initialize the storage
12
- import { __testing__ } from './internal/context/async-local-storage.mjs'
13
- __testing__.forceSyncMode()
14
-
15
- // Re-export everything from the main entry
16
11
  export * from './index.mjs'
@@ -302,15 +302,21 @@ export class ScopedContainer implements IContainer {
302
302
  // Check if we already have this instance (or one is being created)
303
303
  const existingHolder = this.requestContextHolder.get(instanceName)
304
304
  if (existingHolder) {
305
- // Wait for the holder to be ready if it's still being created
306
- // This prevents race conditions where multiple concurrent calls
307
- // might try to create the same service
308
- const [error, readyHolder] =
309
- await BaseHolderManager.waitForHolderReady(existingHolder)
310
- if (error) {
311
- throw error
305
+ // If the holder is in error state, remove it so we can retry
306
+ if (existingHolder.status === InstanceStatus.Error) {
307
+ this.requestContextHolder.delete(instanceName)
308
+ // Fall through to create a new instance
309
+ } else {
310
+ // Wait for the holder to be ready if it's still being created
311
+ // This prevents race conditions where multiple concurrent calls
312
+ // might try to create the same service
313
+ const [error, readyHolder] =
314
+ await BaseHolderManager.waitForHolderReady(existingHolder)
315
+ if (error) {
316
+ throw error
317
+ }
318
+ return readyHolder.instance
312
319
  }
313
- return readyHolder.instance
314
320
  }
315
321
 
316
322
  // Create new instance using parent's resolution mechanism
package/src/index.mts CHANGED
@@ -36,7 +36,11 @@ export {
36
36
  type InjectionTokenSchemaType,
37
37
  } from './token/injection-token.mjs'
38
38
 
39
- export { Registry, globalRegistry, type FactoryRecord } from './token/registry.mjs'
39
+ export {
40
+ Registry,
41
+ globalRegistry,
42
+ type FactoryRecord,
43
+ } from './token/registry.mjs'
40
44
 
41
45
  // ============================================================================
42
46
  // PUBLIC API - Decorators
@@ -93,12 +97,6 @@ export {
93
97
  provideFactoryContext,
94
98
  } from './injectors.mjs'
95
99
 
96
- // ============================================================================
97
- // PUBLIC API - Testing
98
- // ============================================================================
99
-
100
- export * from './testing/index.mjs'
101
-
102
100
  // ============================================================================
103
101
  // INTERNAL API (exported for advanced use cases)
104
102
  // ============================================================================
@@ -113,7 +111,6 @@ export {
113
111
  } from './internal/context/request-context.mjs'
114
112
  export {
115
113
  type ResolutionContextData,
116
- resolutionContext,
117
114
  withResolutionContext,
118
115
  getCurrentResolutionContext,
119
116
  withoutResolutionContext,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Browser implementation using SyncLocalStorage.
3
+ *
4
+ * This module is used in browser environments where async_hooks is not available.
5
+ * It provides synchronous-only context tracking which is sufficient for
6
+ * browser-based DI resolution.
7
+ */
8
+
9
+ import { SyncLocalStorage } from './sync-local-storage.mjs'
10
+
11
+ export type { IAsyncLocalStorage } from './async-local-storage.types.mjs'
12
+
13
+ export function createAsyncLocalStorage<T>() {
14
+ return new SyncLocalStorage<T>()
15
+ }
16
+
17
+ export function isUsingNativeAsyncLocalStorage(): boolean {
18
+ return false
19
+ }
@@ -1,120 +1,68 @@
1
- /**
2
- * Cross-platform AsyncLocalStorage wrapper.
3
- *
4
- * Provides AsyncLocalStorage on Node.js/Bun and falls back to
5
- * a synchronous-only polyfill in browser environments.
6
- */
7
-
8
- import { SyncLocalStorage } from './sync-local-storage.mjs'
9
-
10
- /**
11
- * Interface matching the subset of AsyncLocalStorage API we use.
12
- */
13
- export interface IAsyncLocalStorage<T> {
14
- run<R>(store: T, fn: () => R): R
15
- getStore(): T | undefined
16
- }
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
17
2
 
18
3
  /**
19
- * Detects if we're running in a Node.js-like environment with async_hooks support.
4
+ * Cross-platform AsyncLocalStorage switcher.
5
+ *
6
+ * Provides the appropriate implementation based on environment:
7
+ * - Production: No-op implementation (circular detection disabled)
8
+ * - Development: Native AsyncLocalStorage from node:async_hooks
9
+ *
10
+ * Browser environments use a separate entry point via package.json exports
11
+ * that directly uses SyncLocalStorage.
12
+ *
13
+ * Uses lazy initialization to avoid import overhead until first use,
14
+ * and works with both ESM and CJS builds.
20
15
  */
21
- function hasAsyncHooksSupport(): boolean {
22
- // Check for Node.js
23
- if (
24
- typeof process !== 'undefined' &&
25
- process.versions &&
26
- process.versions.node
27
- ) {
28
- return true
29
- }
30
16
 
31
- // Check for Bun
32
- if (typeof process !== 'undefined' && process.versions && 'bun' in process.versions) {
33
- return true
34
- }
17
+ import type { IAsyncLocalStorage } from './async-local-storage.types.mjs'
35
18
 
36
- // Check for Deno (also supports async_hooks via node compat)
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- if (typeof (globalThis as any).Deno !== 'undefined') {
39
- return true
40
- }
19
+ export type { IAsyncLocalStorage }
41
20
 
42
- return false
43
- }
21
+ const isProduction = process.env.NODE_ENV === 'production'
44
22
 
45
- // Cache for the AsyncLocalStorage class
46
- let AsyncLocalStorageClass: (new <T>() => IAsyncLocalStorage<T>) | null = null
47
- let initialized = false
48
- let forceSyncMode = false
23
+ // Lazy-loaded module cache
24
+ let loadedModule: {
25
+ createAsyncLocalStorage: <T>() => IAsyncLocalStorage<T>
26
+ isUsingNativeAsyncLocalStorage: () => boolean
27
+ } | null = null
49
28
 
50
- /**
51
- * Gets the appropriate AsyncLocalStorage implementation for the current environment.
52
- *
53
- * - On Node.js/Bun/Deno: Returns the native AsyncLocalStorage
54
- * - On browsers: Returns SyncLocalStorage polyfill
55
- */
56
- function getAsyncLocalStorageClass(): new <T>() => IAsyncLocalStorage<T> {
57
- if (initialized) {
58
- return AsyncLocalStorageClass!
29
+ function getModule() {
30
+ if (loadedModule) {
31
+ return loadedModule
59
32
  }
60
33
 
61
- initialized = true
34
+ if (isProduction) {
35
+ // In production, use the noop implementation
36
+ // Inline to avoid any import overhead
37
+ class NoopLocalStorage<T> implements IAsyncLocalStorage<T> {
38
+ run<R>(_store: T, fn: () => R): R {
39
+ return fn()
40
+ }
41
+ getStore(): T | undefined {
42
+ return undefined
43
+ }
44
+ }
62
45
 
63
- if (!forceSyncMode && hasAsyncHooksSupport()) {
64
- try {
65
- // Dynamic require to avoid bundler issues
66
- // eslint-disable-next-line @typescript-eslint/no-require-imports
67
- const asyncHooks = require('node:async_hooks')
68
- AsyncLocalStorageClass = asyncHooks.AsyncLocalStorage
69
- } catch {
70
- // Fallback if require fails (shouldn't happen in Node/Bun)
71
- AsyncLocalStorageClass = SyncLocalStorage as any
46
+ loadedModule = {
47
+ createAsyncLocalStorage: <T,>() => new NoopLocalStorage<T>(),
48
+ isUsingNativeAsyncLocalStorage: () => false,
72
49
  }
73
50
  } else {
74
- AsyncLocalStorageClass = SyncLocalStorage as any
51
+ // In development, use native AsyncLocalStorage
52
+
53
+ loadedModule = {
54
+ createAsyncLocalStorage: <T,>() => new AsyncLocalStorage<T>(),
55
+ isUsingNativeAsyncLocalStorage: () => true,
56
+ }
75
57
  }
76
58
 
77
- return AsyncLocalStorageClass!
59
+ return loadedModule
78
60
  }
79
61
 
80
- /**
81
- * Creates a new AsyncLocalStorage instance appropriate for the current environment.
82
- */
83
62
  export function createAsyncLocalStorage<T>(): IAsyncLocalStorage<T> {
84
- const StorageClass = getAsyncLocalStorageClass()
85
- return new StorageClass<T>()
63
+ return getModule().createAsyncLocalStorage<T>()
86
64
  }
87
65
 
88
- /**
89
- * Returns true if we're using the real AsyncLocalStorage (Node/Bun/Deno).
90
- * Returns false if we're using the sync-only polyfill (browser).
91
- */
92
66
  export function isUsingNativeAsyncLocalStorage(): boolean {
93
- getAsyncLocalStorageClass() // Ensure initialized
94
- return AsyncLocalStorageClass !== (SyncLocalStorage as any)
95
- }
96
-
97
- /**
98
- * Testing utilities for forcing specific modes.
99
- * Only exported for testing purposes.
100
- */
101
- export const __testing__ = {
102
- /**
103
- * Resets the initialization state and forces sync mode.
104
- * Call this before creating new storage instances in tests.
105
- */
106
- forceSyncMode: () => {
107
- initialized = false
108
- forceSyncMode = true
109
- AsyncLocalStorageClass = null
110
- },
111
-
112
- /**
113
- * Resets to default behavior (auto-detect environment).
114
- */
115
- reset: () => {
116
- initialized = false
117
- forceSyncMode = false
118
- AsyncLocalStorageClass = null
119
- },
67
+ return getModule().isUsingNativeAsyncLocalStorage()
120
68
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Interface matching the subset of AsyncLocalStorage API we use.
3
+ */
4
+ export interface IAsyncLocalStorage<T> {
5
+ run<R>(store: T, fn: () => R): R
6
+ getStore(): T | undefined
7
+ }
@@ -1,4 +1,5 @@
1
1
  import type { InstanceHolder } from '../holder/instance-holder.mjs'
2
+ import type { IAsyncLocalStorage } from './async-local-storage.types.mjs'
2
3
 
3
4
  import { createAsyncLocalStorage } from './async-local-storage.mjs'
4
5
 
@@ -18,8 +19,20 @@ export interface ResolutionContextData {
18
19
  * This allows tracking which service is being instantiated even across
19
20
  * async boundaries (like when inject() is called inside a constructor).
20
21
  * Essential for circular dependency detection.
22
+ *
23
+ * The actual implementation varies by environment:
24
+ * - Production: No-op (returns undefined, run() just calls fn directly)
25
+ * - Development: Real AsyncLocalStorage with full async tracking
26
+ * - Browser: SyncLocalStorage for synchronous-only tracking
21
27
  */
22
- export const resolutionContext = createAsyncLocalStorage<ResolutionContextData>()
28
+ let resolutionContext: IAsyncLocalStorage<ResolutionContextData> | null = null
29
+
30
+ function getResolutionContext(): IAsyncLocalStorage<ResolutionContextData> {
31
+ if (!resolutionContext) {
32
+ resolutionContext = createAsyncLocalStorage<ResolutionContextData>()
33
+ }
34
+ return resolutionContext
35
+ }
23
36
 
24
37
  /**
25
38
  * Runs a function within a resolution context.
@@ -36,7 +49,7 @@ export function withResolutionContext<T>(
36
49
  getHolder: (name: string) => InstanceHolder | undefined,
37
50
  fn: () => T,
38
51
  ): T {
39
- return resolutionContext.run({ waiterHolder, getHolder }, fn)
52
+ return getResolutionContext().run({ waiterHolder, getHolder }, fn)
40
53
  }
41
54
 
42
55
  /**
@@ -45,8 +58,10 @@ export function withResolutionContext<T>(
45
58
  * Returns undefined if we're not inside a resolution context
46
59
  * (e.g., when resolving a top-level service that has no parent).
47
60
  */
48
- export function getCurrentResolutionContext(): ResolutionContextData | undefined {
49
- return resolutionContext.getStore()
61
+ export function getCurrentResolutionContext():
62
+ | ResolutionContextData
63
+ | undefined {
64
+ return getResolutionContext().getStore()
50
65
  }
51
66
 
52
67
  /**
@@ -59,5 +74,8 @@ export function getCurrentResolutionContext(): ResolutionContextData | undefined
59
74
  */
60
75
  export function withoutResolutionContext<T>(fn: () => T): T {
61
76
  // Run with undefined context to clear any current context
62
- return resolutionContext.run(undefined as any, fn)
77
+ return getResolutionContext().run(
78
+ undefined as unknown as ResolutionContextData,
79
+ fn,
80
+ )
63
81
  }
@@ -1,3 +1,5 @@
1
+ import type { IAsyncLocalStorage } from './async-local-storage.types.mjs'
2
+
1
3
  /**
2
4
  * A synchronous-only polyfill for AsyncLocalStorage.
3
5
  *
@@ -12,7 +14,7 @@
12
14
  * 1. Constructors are typically synchronous
13
15
  * 2. Circular dependency detection mainly needs sync tracking
14
16
  */
15
- export class SyncLocalStorage<T> {
17
+ export class SyncLocalStorage<T> implements IAsyncLocalStorage<T> {
16
18
  private stack: T[] = []
17
19
 
18
20
  /**
@@ -213,7 +213,14 @@ export class InstanceResolver {
213
213
  return null // Proceed with creation
214
214
 
215
215
  default:
216
- return [error]
216
+ // For error states, remove the failed holder from storage so we can retry
217
+ if (holder) {
218
+ this.logger?.log(
219
+ `[InstanceResolver] Removing failed instance ${instanceName} from storage to allow retry`,
220
+ )
221
+ storage.delete(instanceName)
222
+ }
223
+ return null // Proceed with creation
217
224
  }
218
225
  }
219
226
 
@@ -1,11 +1,19 @@
1
1
  import type { InstanceHolder } from '../holder/instance-holder.mjs'
2
2
 
3
+ /**
4
+ * Whether we're running in production mode.
5
+ * In production, circular dependency detection is skipped for performance.
6
+ */
7
+ const isProduction = process.env.NODE_ENV === 'production'
8
+
3
9
  /**
4
10
  * Detects circular dependencies by analyzing the waitingFor relationships
5
11
  * between service holders.
6
12
  *
7
13
  * Uses BFS to traverse the waitingFor graph starting from a target holder
8
14
  * and checks if following the chain leads back to the waiter, indicating a circular dependency.
15
+ *
16
+ * Note: In production (NODE_ENV === 'production'), detection is skipped for performance.
9
17
  */
10
18
  export class CircularDetector {
11
19
  /**
@@ -14,6 +22,8 @@ export class CircularDetector {
14
22
  * This works by checking if `targetName` (or any holder in its waitingFor chain)
15
23
  * is currently waiting for `waiterName`. If so, waiting would create a deadlock.
16
24
  *
25
+ * In production mode, this always returns null to skip the BFS traversal overhead.
26
+ *
17
27
  * @param waiterName The name of the holder that wants to wait
18
28
  * @param targetName The name of the holder being waited on
19
29
  * @param getHolder Function to retrieve a holder by name
@@ -24,6 +34,11 @@ export class CircularDetector {
24
34
  targetName: string,
25
35
  getHolder: (name: string) => InstanceHolder | undefined,
26
36
  ): string[] | null {
37
+ // Skip circular dependency detection in production for performance
38
+ if (isProduction) {
39
+ return null
40
+ }
41
+
27
42
  // Use BFS to find if there's a path from targetName back to waiterName
28
43
  const visited = new Set<string>()
29
44
  const queue: Array<{ name: string; path: string[] }> = [
@@ -49,6 +49,27 @@ export class Registry {
49
49
  delete(token: InjectionToken<any, any>) {
50
50
  this.factories.delete(token.id)
51
51
  }
52
+
53
+ /**
54
+ * Updates the scope of an already registered factory.
55
+ * This is useful when you need to dynamically change a service's scope
56
+ * (e.g., when a singleton controller has request-scoped dependencies).
57
+ *
58
+ * @param token The injection token to update
59
+ * @param scope The new scope to set
60
+ * @returns true if the scope was updated, false if the token was not found
61
+ */
62
+ updateScope(token: InjectionToken<any, any>, scope: InjectableScope): boolean {
63
+ const factory = this.factories.get(token.id)
64
+ if (factory) {
65
+ factory.scope = scope
66
+ return true
67
+ }
68
+ if (this.parent) {
69
+ return this.parent.updateScope(token, scope)
70
+ }
71
+ return false
72
+ }
52
73
  }
53
74
 
54
75
  export const globalRegistry = new Registry()
package/tsdown.config.mts CHANGED
@@ -33,7 +33,7 @@ export default defineConfig([
33
33
  ),
34
34
  ],
35
35
  },
36
- // Browser build - uses dedicated entry that forces SyncLocalStorage
36
+ // Browser build - uses SyncLocalStorage and skips async_hooks entirely
37
37
  {
38
38
  entry: {
39
39
  'browser/index': 'src/browser.mts',
@@ -45,6 +45,17 @@ export default defineConfig([
45
45
  platform: 'browser',
46
46
  dts: true,
47
47
  target: 'es2022',
48
+ inputOptions(options) {
49
+ return {
50
+ ...options,
51
+ resolve: {
52
+ ...options.resolve,
53
+ alias: {
54
+ './async-local-storage.mjs': './async-local-storage.browser.mjs',
55
+ },
56
+ },
57
+ }
58
+ },
48
59
  plugins: [
49
60
  withFilter(
50
61
  swc.rolldown({
package/vitest.config.mts CHANGED
@@ -2,8 +2,30 @@ import { defineProject } from 'vitest/config'
2
2
 
3
3
  export default defineProject({
4
4
  test: {
5
- typecheck: {
6
- enabled: true,
7
- },
5
+ projects: [
6
+ {
7
+ test: {
8
+ include: ['src/**/__tests__/**/*.spec.mts'],
9
+ exclude: ['src/**/__tests__/**/*.browser.spec.mts'],
10
+ },
11
+ },
12
+ {
13
+ test: {
14
+ include: ['src/**/__tests__/**/*.browser.spec.mts'],
15
+ },
16
+ resolve: {
17
+ alias: {
18
+ './async-local-storage.mjs': './async-local-storage.browser.mjs',
19
+ },
20
+ },
21
+ },
22
+ {
23
+ test: {
24
+ typecheck: {
25
+ enabled: true,
26
+ },
27
+ },
28
+ },
29
+ ],
8
30
  },
9
31
  })