@navios/di 0.5.1 → 0.6.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 (122) hide show
  1. package/CHANGELOG.md +145 -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 +3013 -0
  15. package/lib/browser/index.mjs.map +1 -0
  16. package/lib/index-7jfWsiG4.d.mts +1211 -0
  17. package/lib/index-7jfWsiG4.d.mts.map +1 -0
  18. package/lib/index-DW3K5sOX.d.cts +1206 -0
  19. package/lib/index-DW3K5sOX.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-BG_fa9TJ.mjs +2656 -0
  33. package/lib/testing-BG_fa9TJ.mjs.map +1 -0
  34. package/lib/testing-DIaIRiJz.cjs +2896 -0
  35. package/lib/testing-DIaIRiJz.cjs.map +1 -0
  36. package/package.json +29 -7
  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__/factory.spec.mts +1 -1
  44. package/src/__tests__/get-injectors.spec.mts +1 -1
  45. package/src/__tests__/injectable.spec.mts +1 -1
  46. package/src/__tests__/injection-token.spec.mts +1 -1
  47. package/src/__tests__/library-findings.spec.mts +563 -0
  48. package/src/__tests__/registry.spec.mts +2 -2
  49. package/src/__tests__/request-scope.spec.mts +266 -274
  50. package/src/__tests__/service-instantiator.spec.mts +18 -17
  51. package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
  52. package/src/__tests__/service-locator-manager.spec.mts +15 -15
  53. package/src/__tests__/service-locator.spec.mts +167 -244
  54. package/src/__tests__/unified-api.spec.mts +27 -27
  55. package/src/__type-tests__/factory.spec-d.mts +2 -2
  56. package/src/__type-tests__/inject.spec-d.mts +2 -2
  57. package/src/__type-tests__/injectable.spec-d.mts +1 -1
  58. package/src/browser.mts +16 -0
  59. package/src/container/container.mts +319 -0
  60. package/src/container/index.mts +2 -0
  61. package/src/container/scoped-container.mts +350 -0
  62. package/src/decorators/factory.decorator.mts +4 -4
  63. package/src/decorators/injectable.decorator.mts +5 -5
  64. package/src/errors/di-error.mts +12 -5
  65. package/src/errors/index.mts +0 -8
  66. package/src/index.mts +156 -15
  67. package/src/interfaces/container.interface.mts +82 -0
  68. package/src/interfaces/factory.interface.mts +2 -2
  69. package/src/interfaces/index.mts +1 -0
  70. package/src/internal/context/async-local-storage.mts +120 -0
  71. package/src/internal/context/factory-context.mts +18 -0
  72. package/src/internal/context/index.mts +3 -0
  73. package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
  74. package/src/internal/context/resolution-context.mts +63 -0
  75. package/src/internal/context/sync-local-storage.mts +51 -0
  76. package/src/internal/core/index.mts +5 -0
  77. package/src/internal/core/instance-resolver.mts +641 -0
  78. package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
  79. package/src/internal/core/invalidator.mts +437 -0
  80. package/src/internal/core/service-locator.mts +202 -0
  81. package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
  82. package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
  83. package/src/internal/holder/holder-manager.mts +85 -0
  84. package/src/internal/holder/holder-storage.interface.mts +116 -0
  85. package/src/internal/holder/index.mts +6 -0
  86. package/src/internal/holder/instance-holder.mts +109 -0
  87. package/src/internal/holder/request-storage.mts +134 -0
  88. package/src/internal/holder/singleton-storage.mts +105 -0
  89. package/src/internal/index.mts +4 -0
  90. package/src/internal/lifecycle/circular-detector.mts +77 -0
  91. package/src/internal/lifecycle/index.mts +2 -0
  92. package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +11 -4
  93. package/src/testing/__tests__/test-container.spec.mts +2 -2
  94. package/src/testing/test-container.mts +4 -4
  95. package/src/token/index.mts +2 -0
  96. package/src/{injection-token.mts → token/injection-token.mts} +1 -1
  97. package/src/{registry.mts → token/registry.mts} +1 -1
  98. package/src/utils/get-injectable-token.mts +1 -1
  99. package/src/utils/get-injectors.mts +32 -15
  100. package/src/utils/types.mts +1 -1
  101. package/tsdown.config.mts +67 -0
  102. package/lib/_tsup-dts-rollup.d.mts +0 -1283
  103. package/lib/_tsup-dts-rollup.d.ts +0 -1283
  104. package/lib/chunk-2M576LCC.mjs +0 -2043
  105. package/lib/chunk-2M576LCC.mjs.map +0 -1
  106. package/lib/index.d.ts +0 -78
  107. package/lib/index.js +0 -2127
  108. package/lib/index.js.map +0 -1
  109. package/lib/testing/index.d.ts +0 -2
  110. package/lib/testing/index.js +0 -2060
  111. package/lib/testing/index.js.map +0 -1
  112. package/lib/testing/index.mjs.map +0 -1
  113. package/src/container.mts +0 -227
  114. package/src/factory-context.mts +0 -8
  115. package/src/instance-resolver.mts +0 -559
  116. package/src/request-context-manager.mts +0 -149
  117. package/src/service-invalidator.mts +0 -429
  118. package/src/service-locator-instance-holder.mts +0 -70
  119. package/src/service-locator-manager.mts +0 -85
  120. package/src/service-locator.mts +0 -246
  121. package/tsup.config.mts +0 -12
  122. /package/src/{injector.mts → injectors.mts} +0 -0
@@ -0,0 +1,116 @@
1
+ import type { InjectableScope, InjectableType } from '../../enums/index.mjs'
2
+ import type { DIError } from '../../errors/index.mjs'
3
+ import type { InstanceHolder } from './instance-holder.mjs'
4
+
5
+ /**
6
+ * Result type for holder retrieval operations.
7
+ * - [undefined, holder] - Holder found successfully
8
+ * - [DIError, holder?] - Error occurred (holder may be available for waiting)
9
+ * - null - No holder exists
10
+ */
11
+ export type HolderGetResult<T = unknown> =
12
+ | [undefined, InstanceHolder<T>]
13
+ | [DIError, InstanceHolder<T>?]
14
+ | null
15
+
16
+ /**
17
+ * Interface for abstracting holder storage operations.
18
+ *
19
+ * Enables unified instance resolution logic regardless of where
20
+ * holders are stored (singleton manager, request context, etc.).
21
+ * This is the key abstraction for the Storage Strategy pattern.
22
+ */
23
+ export interface IHolderStorage {
24
+ /**
25
+ * The scope this storage handles.
26
+ */
27
+ readonly scope: InjectableScope
28
+
29
+ // ============================================================================
30
+ // BASIC OPERATIONS
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Retrieves an existing holder by instance name.
35
+ *
36
+ * @param instanceName The unique identifier for the instance
37
+ * @returns
38
+ * - [undefined, holder] if found and ready/creating
39
+ * - [DIError, holder?] if found but in error/destroying state
40
+ * - null if not found
41
+ */
42
+ get<T = unknown>(instanceName: string): HolderGetResult<T>
43
+
44
+ /**
45
+ * Stores a holder by instance name.
46
+ *
47
+ * @param instanceName The unique identifier for the instance
48
+ * @param holder The holder to store
49
+ */
50
+ set(instanceName: string, holder: InstanceHolder): void
51
+
52
+ /**
53
+ * Deletes a holder by instance name.
54
+ *
55
+ * @param instanceName The unique identifier for the instance
56
+ * @returns true if the holder was deleted, false if it didn't exist
57
+ */
58
+ delete(instanceName: string): boolean
59
+
60
+ /**
61
+ * Creates a new holder in "Creating" state with a deferred promise.
62
+ * The holder is NOT automatically stored - call set() to store it.
63
+ *
64
+ * @param instanceName The unique identifier for the instance
65
+ * @param type The injectable type
66
+ * @param deps The set of dependency names
67
+ * @returns A tuple containing the deferred promise resolver and the holder
68
+ */
69
+ createHolder<T>(
70
+ instanceName: string,
71
+ type: InjectableType,
72
+ deps: Set<string>,
73
+ ): [
74
+ ReturnType<typeof Promise.withResolvers<[undefined, T]>>,
75
+ InstanceHolder<T>,
76
+ ]
77
+
78
+ /**
79
+ * Checks if this storage should be used for the given scope.
80
+ */
81
+ handles(scope: InjectableScope): boolean
82
+
83
+ // ============================================================================
84
+ // ITERATION AND QUERY
85
+ // ============================================================================
86
+
87
+ /**
88
+ * Gets all instance names in this storage.
89
+ */
90
+ getAllNames(): string[]
91
+
92
+ /**
93
+ * Iterates over all holders with a callback.
94
+ *
95
+ * @param callback Function called for each holder with (name, holder)
96
+ */
97
+ forEach(
98
+ callback: (name: string, holder: InstanceHolder) => void,
99
+ ): void
100
+
101
+ /**
102
+ * Finds a holder by its instance value (reverse lookup).
103
+ *
104
+ * @param instance The instance to search for
105
+ * @returns The holder if found, null otherwise
106
+ */
107
+ findByInstance(instance: unknown): InstanceHolder | null
108
+
109
+ /**
110
+ * Finds all instance names that depend on the given instance name.
111
+ *
112
+ * @param instanceName The instance name to find dependents for
113
+ * @returns Array of instance names that have this instance as a dependency
114
+ */
115
+ findDependents(instanceName: string): string[]
116
+ }
@@ -0,0 +1,6 @@
1
+ export * from './instance-holder.mjs'
2
+ export * from './base-holder-manager.mjs'
3
+ export * from './holder-manager.mjs'
4
+ export * from './holder-storage.interface.mjs'
5
+ export * from './singleton-storage.mjs'
6
+ export * from './request-storage.mjs'
@@ -0,0 +1,109 @@
1
+ import type { InjectableScope, InjectableType } from '../../enums/index.mjs'
2
+
3
+ /**
4
+ * Represents the lifecycle status of an instance holder.
5
+ */
6
+ export enum InstanceStatus {
7
+ /** Instance has been successfully created and is ready for use */
8
+ Created = 'created',
9
+ /** Instance is currently being created (async initialization in progress) */
10
+ Creating = 'creating',
11
+ /** Instance is being destroyed (cleanup in progress) */
12
+ Destroying = 'destroying',
13
+ /** Instance creation failed with an error */
14
+ Error = 'error',
15
+ }
16
+
17
+ /** Callback function for instance effects */
18
+ export type InstanceEffect = () => void
19
+
20
+ /** Callback function for instance destruction listeners */
21
+ export type InstanceDestroyListener = () => void | Promise<void>
22
+
23
+ /**
24
+ * Instance holder in the Creating state.
25
+ * The instance is null while creation is in progress.
26
+ */
27
+ export interface InstanceHolderCreating<Instance> {
28
+ status: InstanceStatus.Creating
29
+ name: string
30
+ instance: null
31
+ creationPromise: Promise<[undefined, Instance]> | null
32
+ destroyPromise: null
33
+ type: InjectableType
34
+ scope: InjectableScope
35
+ deps: Set<string>
36
+ destroyListeners: InstanceDestroyListener[]
37
+ createdAt: number
38
+ /** Tracks which services this holder is currently waiting for (for circular dependency detection) */
39
+ waitingFor: Set<string>
40
+ }
41
+
42
+ /**
43
+ * Instance holder in the Created state.
44
+ * The instance is available and ready for use.
45
+ */
46
+ export interface InstanceHolderCreated<Instance> {
47
+ status: InstanceStatus.Created
48
+ name: string
49
+ instance: Instance
50
+ creationPromise: null
51
+ destroyPromise: null
52
+ type: InjectableType
53
+ scope: InjectableScope
54
+ deps: Set<string>
55
+ destroyListeners: InstanceDestroyListener[]
56
+ createdAt: number
57
+ /** Tracks which services this holder is currently waiting for (for circular dependency detection) */
58
+ waitingFor: Set<string>
59
+ }
60
+
61
+ /**
62
+ * Instance holder in the Destroying state.
63
+ * The instance may still be available but is being cleaned up.
64
+ */
65
+ export interface InstanceHolderDestroying<Instance> {
66
+ status: InstanceStatus.Destroying
67
+ name: string
68
+ instance: Instance | null
69
+ creationPromise: null
70
+ destroyPromise: Promise<void>
71
+ type: InjectableType
72
+ scope: InjectableScope
73
+ deps: Set<string>
74
+ destroyListeners: InstanceDestroyListener[]
75
+ createdAt: number
76
+ /** Tracks which services this holder is currently waiting for (for circular dependency detection) */
77
+ waitingFor: Set<string>
78
+ }
79
+
80
+ /**
81
+ * Instance holder in the Error state.
82
+ * The instance field contains the error that occurred during creation.
83
+ */
84
+ export interface InstanceHolderError {
85
+ status: InstanceStatus.Error
86
+ name: string
87
+ instance: Error
88
+ creationPromise: null
89
+ destroyPromise: null
90
+ type: InjectableType
91
+ scope: InjectableScope
92
+ deps: Set<string>
93
+ destroyListeners: InstanceDestroyListener[]
94
+ createdAt: number
95
+ /** Tracks which services this holder is currently waiting for (for circular dependency detection) */
96
+ waitingFor: Set<string>
97
+ }
98
+
99
+ /**
100
+ * Holds the state of a service instance throughout its lifecycle.
101
+ *
102
+ * Tracks creation/destruction promises, dependency relationships,
103
+ * destroy listeners, and current status (Creating, Created, Destroying, Error).
104
+ */
105
+ export type InstanceHolder<Instance = unknown> =
106
+ | InstanceHolderCreating<Instance>
107
+ | InstanceHolderCreated<Instance>
108
+ | InstanceHolderDestroying<Instance>
109
+ | InstanceHolderError
@@ -0,0 +1,134 @@
1
+ import type { RequestContext } from '../context/request-context.mjs'
2
+ import type { BaseHolderManager } from './base-holder-manager.mjs'
3
+ import type {
4
+ HolderGetResult,
5
+ IHolderStorage,
6
+ } from './holder-storage.interface.mjs'
7
+ import type { InstanceHolder } from './instance-holder.mjs'
8
+
9
+ import { InjectableScope, InjectableType } from '../../enums/index.mjs'
10
+ import { DIError } from '../../errors/index.mjs'
11
+ import { InstanceStatus } from './instance-holder.mjs'
12
+
13
+ /**
14
+ * Storage implementation for Request-scoped services.
15
+ *
16
+ * Wraps a RequestContext instance from a ScopedContainer and provides
17
+ * the IHolderStorage interface. This allows the InstanceResolver to work
18
+ * with request-scoped storage using the same interface as singleton storage.
19
+ */
20
+ export class RequestStorage implements IHolderStorage {
21
+ readonly scope = InjectableScope.Request
22
+
23
+ constructor(
24
+ private readonly contextHolder: RequestContext,
25
+ private readonly holderManager: BaseHolderManager,
26
+ ) {}
27
+
28
+ get<T = unknown>(instanceName: string): HolderGetResult<T> {
29
+ const holder = this.contextHolder.get(instanceName)
30
+
31
+ if (!holder) {
32
+ return null
33
+ }
34
+
35
+ // Check holder status for error states
36
+ switch (holder.status) {
37
+ case InstanceStatus.Destroying:
38
+ return [
39
+ DIError.instanceDestroying(instanceName),
40
+ holder as InstanceHolder<T>,
41
+ ]
42
+
43
+ case InstanceStatus.Error:
44
+ return [
45
+ holder.instance as unknown as DIError,
46
+ holder as InstanceHolder<T>,
47
+ ]
48
+
49
+ case InstanceStatus.Creating:
50
+ case InstanceStatus.Created:
51
+ return [undefined, holder as InstanceHolder<T>]
52
+
53
+ default:
54
+ return null
55
+ }
56
+ }
57
+
58
+ set(instanceName: string, holder: InstanceHolder): void {
59
+ this.contextHolder.set(instanceName, holder)
60
+ }
61
+
62
+ delete(instanceName: string): boolean {
63
+ return this.contextHolder.delete(instanceName)
64
+ }
65
+
66
+ createHolder<T>(
67
+ instanceName: string,
68
+ type: InjectableType,
69
+ deps: Set<string>,
70
+ ): [
71
+ ReturnType<typeof Promise.withResolvers<[undefined, T]>>,
72
+ InstanceHolder<T>,
73
+ ] {
74
+ // Use the holderManager's createCreatingHolder method
75
+ // which is inherited from BaseHolderManager
76
+ return this.holderManager.createCreatingHolder<T>(
77
+ instanceName,
78
+ type,
79
+ this.scope,
80
+ deps,
81
+ )
82
+ }
83
+
84
+ handles(scope: InjectableScope): boolean {
85
+ return scope === InjectableScope.Request
86
+ }
87
+
88
+ // ============================================================================
89
+ // ITERATION AND QUERY
90
+ // ============================================================================
91
+
92
+ getAllNames(): string[] {
93
+ const names: string[] = []
94
+ for (const [name] of this.contextHolder.holders) {
95
+ names.push(name)
96
+ }
97
+ return names
98
+ }
99
+
100
+ forEach(callback: (name: string, holder: InstanceHolder) => void): void {
101
+ for (const [name, holder] of this.contextHolder.holders) {
102
+ callback(name, holder)
103
+ }
104
+ }
105
+
106
+ findByInstance(instance: unknown): InstanceHolder | null {
107
+ for (const holder of this.contextHolder.holders.values()) {
108
+ if (holder.instance === instance) {
109
+ return holder
110
+ }
111
+ }
112
+ return null
113
+ }
114
+
115
+ findDependents(instanceName: string): string[] {
116
+ const dependents: string[] = []
117
+
118
+ // Check request-scoped holders
119
+ for (const [name, holder] of this.contextHolder.holders) {
120
+ if (holder.deps.has(instanceName)) {
121
+ dependents.push(name)
122
+ }
123
+ }
124
+
125
+ // Also check singleton holders - a singleton may depend on this request-scoped service
126
+ for (const [name, holder] of this.holderManager.filter(() => true)) {
127
+ if (holder.deps.has(instanceName)) {
128
+ dependents.push(name)
129
+ }
130
+ }
131
+
132
+ return dependents
133
+ }
134
+ }
@@ -0,0 +1,105 @@
1
+ import type { HolderManager } from './holder-manager.mjs'
2
+ import type {
3
+ HolderGetResult,
4
+ IHolderStorage,
5
+ } from './holder-storage.interface.mjs'
6
+ import type { InstanceHolder } from './instance-holder.mjs'
7
+
8
+ import { InjectableScope, InjectableType } from '../../enums/index.mjs'
9
+ import { DIErrorCode } from '../../errors/index.mjs'
10
+
11
+ /**
12
+ * Storage implementation for Singleton-scoped services.
13
+ *
14
+ * Wraps a HolderManager instance and provides the IHolderStorage interface.
15
+ * This allows the InstanceResolver to work with singleton storage
16
+ * using the same interface as request-scoped storage.
17
+ */
18
+ export class SingletonStorage implements IHolderStorage {
19
+ readonly scope = InjectableScope.Singleton
20
+
21
+ constructor(private readonly manager: HolderManager) {}
22
+
23
+ get<T = unknown>(instanceName: string): HolderGetResult<T> {
24
+ const [error, holder] = this.manager.get(instanceName)
25
+
26
+ if (!error) {
27
+ return [undefined, holder as InstanceHolder<T>]
28
+ }
29
+
30
+ // Handle different error types
31
+ switch (error.code) {
32
+ case DIErrorCode.InstanceNotFound:
33
+ return null
34
+
35
+ case DIErrorCode.InstanceDestroying:
36
+ return [error, holder as InstanceHolder<T> | undefined]
37
+
38
+ default:
39
+ return [error]
40
+ }
41
+ }
42
+
43
+ set(instanceName: string, holder: InstanceHolder): void {
44
+ this.manager.set(instanceName, holder)
45
+ }
46
+
47
+ delete(instanceName: string): boolean {
48
+ return this.manager.delete(instanceName)
49
+ }
50
+
51
+ createHolder<T>(
52
+ instanceName: string,
53
+ type: InjectableType,
54
+ deps: Set<string>,
55
+ ): [
56
+ ReturnType<typeof Promise.withResolvers<[undefined, T]>>,
57
+ InstanceHolder<T>,
58
+ ] {
59
+ return this.manager.createCreatingHolder<T>(
60
+ instanceName,
61
+ type,
62
+ this.scope,
63
+ deps,
64
+ )
65
+ }
66
+
67
+ handles(scope: InjectableScope): boolean {
68
+ return scope === InjectableScope.Singleton
69
+ }
70
+
71
+ // ============================================================================
72
+ // ITERATION AND QUERY
73
+ // ============================================================================
74
+
75
+ getAllNames(): string[] {
76
+ return this.manager.getAllNames()
77
+ }
78
+
79
+ forEach(
80
+ callback: (name: string, holder: InstanceHolder) => void,
81
+ ): void {
82
+ for (const [name, holder] of this.manager.filter(() => true)) {
83
+ callback(name, holder)
84
+ }
85
+ }
86
+
87
+ findByInstance(instance: unknown): InstanceHolder | null {
88
+ for (const [, holder] of this.manager.filter(
89
+ (h) => h.instance === instance,
90
+ )) {
91
+ return holder
92
+ }
93
+ return null
94
+ }
95
+
96
+ findDependents(instanceName: string): string[] {
97
+ const dependents: string[] = []
98
+ for (const [name, holder] of this.manager.filter(() => true)) {
99
+ if (holder.deps.has(instanceName)) {
100
+ dependents.push(name)
101
+ }
102
+ }
103
+ return dependents
104
+ }
105
+ }
@@ -0,0 +1,4 @@
1
+ export * from './core/index.mjs'
2
+ export * from './holder/index.mjs'
3
+ export * from './context/index.mjs'
4
+ export * from './lifecycle/index.mjs'
@@ -0,0 +1,77 @@
1
+ import type { InstanceHolder } from '../holder/instance-holder.mjs'
2
+
3
+ /**
4
+ * Detects circular dependencies by analyzing the waitingFor relationships
5
+ * between service holders.
6
+ *
7
+ * Uses BFS to traverse the waitingFor graph starting from a target holder
8
+ * and checks if following the chain leads back to the waiter, indicating a circular dependency.
9
+ */
10
+ export class CircularDetector {
11
+ /**
12
+ * Detects if waiting for `targetName` from `waiterName` would create a cycle.
13
+ *
14
+ * This works by checking if `targetName` (or any holder in its waitingFor chain)
15
+ * is currently waiting for `waiterName`. If so, waiting would create a deadlock.
16
+ *
17
+ * @param waiterName The name of the holder that wants to wait
18
+ * @param targetName The name of the holder being waited on
19
+ * @param getHolder Function to retrieve a holder by name
20
+ * @returns The cycle path if a cycle is detected, null otherwise
21
+ */
22
+ static detectCycle(
23
+ waiterName: string,
24
+ targetName: string,
25
+ getHolder: (name: string) => InstanceHolder | undefined,
26
+ ): string[] | null {
27
+ // Use BFS to find if there's a path from targetName back to waiterName
28
+ const visited = new Set<string>()
29
+ const queue: Array<{ name: string; path: string[] }> = [
30
+ { name: targetName, path: [waiterName, targetName] },
31
+ ]
32
+
33
+ while (queue.length > 0) {
34
+ const { name: currentName, path } = queue.shift()!
35
+
36
+ // If we've reached back to the waiter, we have a cycle
37
+ if (currentName === waiterName) {
38
+ return path
39
+ }
40
+
41
+ // Skip if already visited
42
+ if (visited.has(currentName)) {
43
+ continue
44
+ }
45
+ visited.add(currentName)
46
+
47
+ // Get the holder and check what it's waiting for
48
+ const holder = getHolder(currentName)
49
+ if (!holder) {
50
+ continue
51
+ }
52
+
53
+ // Add all services this holder is waiting for to the queue
54
+ for (const waitingForName of holder.waitingFor) {
55
+ if (!visited.has(waitingForName)) {
56
+ queue.push({
57
+ name: waitingForName,
58
+ path: [...path, waitingForName],
59
+ })
60
+ }
61
+ }
62
+ }
63
+
64
+ // No path found from target back to waiter, no cycle
65
+ return null
66
+ }
67
+
68
+ /**
69
+ * Formats a cycle path into a human-readable string.
70
+ *
71
+ * @param cycle The cycle path (array of service names)
72
+ * @returns Formatted string like "ServiceA -> ServiceB -> ServiceA"
73
+ */
74
+ static formatCycle(cycle: string[]): string {
75
+ return cycle.join(' -> ')
76
+ }
77
+ }
@@ -0,0 +1,2 @@
1
+ export * from './lifecycle-event-bus.mjs'
2
+ export * from './circular-detector.mjs'
@@ -3,8 +3,15 @@
3
3
 
4
4
  type ListenersMap = Map<string, Map<string, Set<Function>>>
5
5
 
6
+ /**
7
+ * Event bus for service lifecycle events (create, destroy, etc.).
8
+ *
9
+ * Enables loose coupling between services by allowing them to subscribe
10
+ * to lifecycle events of their dependencies without direct references.
11
+ * Used primarily for invalidation cascading.
12
+ */
6
13
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
7
- export class ServiceLocatorEventBus {
14
+ export class LifecycleEventBus {
8
15
  private listeners: ListenersMap = new Map()
9
16
  constructor(private readonly logger: Console | null = null) {}
10
17
 
@@ -13,7 +20,7 @@ export class ServiceLocatorEventBus {
13
20
  event: Event,
14
21
  listener: (event: Event) => void,
15
22
  ) {
16
- this.logger?.debug(`[ServiceLocatorEventBus]#on(): ns:${ns} event:${event}`)
23
+ this.logger?.debug(`[LifecycleEventBus]#on(): ns:${ns} event:${event}`)
17
24
  if (!this.listeners.has(ns)) {
18
25
  this.listeners.set(ns, new Map())
19
26
  }
@@ -43,7 +50,7 @@ export class ServiceLocatorEventBus {
43
50
 
44
51
  const events = this.listeners.get(key)!
45
52
 
46
- this.logger?.debug(`[ServiceLocatorEventBus]#emit(): ${key}:${event}`)
53
+ this.logger?.debug(`[LifecycleEventBus]#emit(): ${key}:${event}`)
47
54
 
48
55
  const res = await Promise.allSettled(
49
56
  [...(events.get(event) ?? [])!].map((listener) => listener(event)),
@@ -52,7 +59,7 @@ export class ServiceLocatorEventBus {
52
59
  .filter((result) => result.status === 'rejected')
53
60
  .map((result: PromiseRejectedResult) => {
54
61
  this.logger?.warn(
55
- `[ServiceLocatorEventBus]#emit(): ${key}:${event} rejected with`,
62
+ `[LifecycleEventBus]#emit(): ${key}:${event} rejected with`,
56
63
  result.reason,
57
64
  )
58
65
  return result
@@ -1,8 +1,8 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest'
2
2
 
3
3
  import { Injectable } from '../../decorators/injectable.decorator.mjs'
4
- import { InjectionToken } from '../../injection-token.mjs'
5
- import { inject } from '../../injector.mjs'
4
+ import { InjectionToken } from '../../token/injection-token.mjs'
5
+ import { inject } from '../../injectors.mjs'
6
6
  import { TestContainer } from '../test-container.mjs'
7
7
 
8
8
  describe('TestContainer', () => {
@@ -1,11 +1,11 @@
1
- import type { ClassType, InjectionToken } from '../injection-token.mjs'
2
- import type { Registry } from '../registry.mjs'
1
+ import type { ClassType, InjectionToken } from '../token/injection-token.mjs'
2
+ import type { Registry } from '../token/registry.mjs'
3
3
  import type { Injectors } from '../utils/index.mjs'
4
4
 
5
- import { Container } from '../container.mjs'
5
+ import { Container } from '../container/container.mjs'
6
6
  import { Injectable } from '../decorators/injectable.decorator.mjs'
7
7
  import { InjectableScope, InjectableType } from '../enums/index.mjs'
8
- import { globalRegistry } from '../registry.mjs'
8
+ import { globalRegistry } from '../token/registry.mjs'
9
9
  import { getInjectableToken } from '../utils/index.mjs'
10
10
 
11
11
  /**
@@ -0,0 +1,2 @@
1
+ export * from './injection-token.mjs'
2
+ export * from './registry.mjs'
@@ -1,6 +1,6 @@
1
1
  import type { z, ZodObject, ZodOptional, ZodRecord } from 'zod/v4'
2
2
 
3
- import type { FactoryContext } from './factory-context.mjs'
3
+ import type { FactoryContext } from '../internal/context/factory-context.mjs'
4
4
 
5
5
  export type ClassType = new (...args: any[]) => any
6
6
  export type ClassTypeWithoutArguments = new () => any
@@ -1,6 +1,6 @@
1
1
  import type { ClassType, InjectionToken } from './injection-token.mjs'
2
2
 
3
- import { InjectableScope, InjectableType } from './enums/index.mjs'
3
+ import { InjectableScope, InjectableType } from '../enums/index.mjs'
4
4
 
5
5
  export type FactoryRecord<Instance = any, Schema = any> = {
6
6
  scope: InjectableScope
@@ -1,4 +1,4 @@
1
- import type { ClassType, InjectionToken } from '../injection-token.mjs'
1
+ import type { ClassType, InjectionToken } from '../token/injection-token.mjs'
2
2
 
3
3
  import { InjectableTokenMeta } from '../symbols/index.mjs'
4
4