@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.
- package/CHANGELOG.md +146 -0
- package/README.md +196 -219
- package/docs/README.md +69 -11
- package/docs/api-reference.md +281 -117
- package/docs/container.md +220 -56
- package/docs/examples/request-scope-example.mts +2 -2
- package/docs/factory.md +3 -8
- package/docs/getting-started.md +37 -8
- package/docs/migration.md +318 -37
- package/docs/request-contexts.md +263 -175
- package/docs/scopes.md +79 -42
- package/lib/browser/index.d.mts +1577 -0
- package/lib/browser/index.d.mts.map +1 -0
- package/lib/browser/index.mjs +3012 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/index-S_qX2VLI.d.mts +1211 -0
- package/lib/index-S_qX2VLI.d.mts.map +1 -0
- package/lib/index-fKPuT65j.d.cts +1206 -0
- package/lib/index-fKPuT65j.d.cts.map +1 -0
- package/lib/index.cjs +389 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +376 -0
- package/lib/index.d.cts.map +1 -0
- package/lib/index.d.mts +371 -78
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +325 -63
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.cjs +9 -0
- package/lib/testing/index.d.cts +2 -0
- package/lib/testing/index.d.mts +2 -2
- package/lib/testing/index.mjs +2 -72
- package/lib/testing-BMGmmxH7.cjs +2895 -0
- package/lib/testing-BMGmmxH7.cjs.map +1 -0
- package/lib/testing-DCXz8AJD.mjs +2655 -0
- package/lib/testing-DCXz8AJD.mjs.map +1 -0
- package/package.json +26 -4
- package/project.json +2 -2
- package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
- package/src/__tests__/async-local-storage.spec.mts +333 -0
- package/src/__tests__/container.spec.mts +30 -25
- package/src/__tests__/e2e.browser.spec.mts +790 -0
- package/src/__tests__/e2e.spec.mts +1222 -0
- package/src/__tests__/errors.spec.mts +6 -6
- package/src/__tests__/factory.spec.mts +1 -1
- package/src/__tests__/get-injectors.spec.mts +1 -1
- package/src/__tests__/injectable.spec.mts +1 -1
- package/src/__tests__/injection-token.spec.mts +1 -1
- package/src/__tests__/library-findings.spec.mts +563 -0
- package/src/__tests__/registry.spec.mts +2 -2
- package/src/__tests__/request-scope.spec.mts +266 -274
- package/src/__tests__/service-instantiator.spec.mts +19 -17
- package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
- package/src/__tests__/service-locator-manager.spec.mts +15 -15
- package/src/__tests__/service-locator.spec.mts +167 -244
- package/src/__tests__/unified-api.spec.mts +27 -27
- package/src/__type-tests__/factory.spec-d.mts +2 -2
- package/src/__type-tests__/inject.spec-d.mts +2 -2
- package/src/__type-tests__/injectable.spec-d.mts +1 -1
- package/src/browser.mts +16 -0
- package/src/container/container.mts +319 -0
- package/src/container/index.mts +2 -0
- package/src/container/scoped-container.mts +350 -0
- package/src/decorators/factory.decorator.mts +4 -4
- package/src/decorators/injectable.decorator.mts +5 -5
- package/src/errors/di-error.mts +13 -7
- package/src/errors/index.mts +0 -8
- package/src/index.mts +156 -15
- package/src/interfaces/container.interface.mts +82 -0
- package/src/interfaces/factory.interface.mts +2 -2
- package/src/interfaces/index.mts +1 -0
- package/src/internal/context/async-local-storage.mts +120 -0
- package/src/internal/context/factory-context.mts +18 -0
- package/src/internal/context/index.mts +3 -0
- package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
- package/src/internal/context/resolution-context.mts +63 -0
- package/src/internal/context/sync-local-storage.mts +51 -0
- package/src/internal/core/index.mts +5 -0
- package/src/internal/core/instance-resolver.mts +641 -0
- package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
- package/src/internal/core/invalidator.mts +437 -0
- package/src/internal/core/service-locator.mts +202 -0
- package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
- package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
- package/src/internal/holder/holder-manager.mts +85 -0
- package/src/internal/holder/holder-storage.interface.mts +116 -0
- package/src/internal/holder/index.mts +6 -0
- package/src/internal/holder/instance-holder.mts +109 -0
- package/src/internal/holder/request-storage.mts +134 -0
- package/src/internal/holder/singleton-storage.mts +105 -0
- package/src/internal/index.mts +4 -0
- package/src/internal/lifecycle/circular-detector.mts +77 -0
- package/src/internal/lifecycle/index.mts +2 -0
- package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +12 -5
- package/src/testing/__tests__/test-container.spec.mts +2 -2
- package/src/testing/test-container.mts +4 -4
- package/src/token/index.mts +2 -0
- package/src/{injection-token.mts → token/injection-token.mts} +1 -1
- package/src/{registry.mts → token/registry.mts} +1 -1
- package/src/utils/get-injectable-token.mts +1 -1
- package/src/utils/get-injectors.mts +32 -15
- package/src/utils/types.mts +1 -1
- package/tsdown.config.mts +67 -0
- package/lib/_tsup-dts-rollup.d.mts +0 -1283
- package/lib/_tsup-dts-rollup.d.ts +0 -1283
- package/lib/chunk-44F3LXW5.mjs +0 -2043
- package/lib/chunk-44F3LXW5.mjs.map +0 -1
- package/lib/index.d.ts +0 -78
- package/lib/index.js +0 -2127
- package/lib/index.js.map +0 -1
- package/lib/testing/index.d.ts +0 -2
- package/lib/testing/index.js +0 -2060
- package/lib/testing/index.js.map +0 -1
- package/lib/testing/index.mjs.map +0 -1
- package/src/container.mts +0 -227
- package/src/factory-context.mts +0 -8
- package/src/instance-resolver.mts +0 -559
- package/src/request-context-manager.mts +0 -149
- package/src/service-invalidator.mts +0 -429
- package/src/service-locator-instance-holder.mts +0 -70
- package/src/service-locator-manager.mts +0 -85
- package/src/service-locator.mts +0 -246
- package/tsup.config.mts +0 -12
- /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,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,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
|
+
}
|
|
@@ -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
|
|
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(`[
|
|
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
|
}
|
|
@@ -26,7 +33,7 @@ export class ServiceLocatorEventBus {
|
|
|
26
33
|
nsEvents.get(event)!.add(listener)
|
|
27
34
|
|
|
28
35
|
return () => {
|
|
29
|
-
nsEvents.get(event)
|
|
36
|
+
nsEvents.get(event)?.delete(listener)
|
|
30
37
|
if (nsEvents.get(event)?.size === 0) {
|
|
31
38
|
nsEvents.delete(event)
|
|
32
39
|
}
|
|
@@ -43,7 +50,7 @@ export class ServiceLocatorEventBus {
|
|
|
43
50
|
|
|
44
51
|
const events = this.listeners.get(key)!
|
|
45
52
|
|
|
46
|
-
this.logger?.debug(`[
|
|
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
|
-
`[
|
|
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 '../../
|
|
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
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { z, ZodObject, ZodOptional, ZodRecord } from 'zod/v4'
|
|
2
2
|
|
|
3
|
-
import type { FactoryContext } from '
|
|
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 '
|
|
3
|
+
import { InjectableScope, InjectableType } from '../enums/index.mjs'
|
|
4
4
|
|
|
5
5
|
export type FactoryRecord<Instance = any, Schema = any> = {
|
|
6
6
|
scope: InjectableScope
|