@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.
- package/CHANGELOG.md +145 -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 +3013 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/index-7jfWsiG4.d.mts +1211 -0
- package/lib/index-7jfWsiG4.d.mts.map +1 -0
- package/lib/index-DW3K5sOX.d.cts +1206 -0
- package/lib/index-DW3K5sOX.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-BG_fa9TJ.mjs +2656 -0
- package/lib/testing-BG_fa9TJ.mjs.map +1 -0
- package/lib/testing-DIaIRiJz.cjs +2896 -0
- package/lib/testing-DIaIRiJz.cjs.map +1 -0
- package/package.json +29 -7
- 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__/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 +18 -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 +12 -5
- 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} +11 -4
- 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-2M576LCC.mjs +0 -2043
- package/lib/chunk-2M576LCC.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
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { FactoryRecord } from '../../token/registry.mjs'
|
|
2
|
+
import type { Injectors } from '../../utils/get-injectors.mjs'
|
|
3
|
+
import type { FactoryContext } from '../context/factory-context.mjs'
|
|
4
4
|
|
|
5
|
-
import { InjectableType } from '
|
|
5
|
+
import { InjectableType } from '../../enums/index.mjs'
|
|
6
|
+
import { DIError } from '../../errors/index.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Creates service instances from registry records.
|
|
10
|
+
*
|
|
11
|
+
* Handles both class-based (@Injectable) and factory-based (@Factory) services,
|
|
12
|
+
* managing the instantiation lifecycle including sync initialization retries
|
|
13
|
+
* and lifecycle hook invocation (onServiceInit, onServiceDestroy).
|
|
10
14
|
*/
|
|
11
|
-
export class
|
|
15
|
+
export class Instantiator {
|
|
12
16
|
constructor(private readonly injectors: Injectors) {}
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -22,7 +26,7 @@ export class ServiceInstantiator {
|
|
|
22
26
|
ctx: FactoryContext,
|
|
23
27
|
record: FactoryRecord<T, any>,
|
|
24
28
|
args: any = undefined,
|
|
25
|
-
): Promise<[undefined, T] | [
|
|
29
|
+
): Promise<[undefined, T] | [DIError]> {
|
|
26
30
|
try {
|
|
27
31
|
switch (record.type) {
|
|
28
32
|
case InjectableType.Class:
|
|
@@ -30,12 +34,12 @@ export class ServiceInstantiator {
|
|
|
30
34
|
case InjectableType.Factory:
|
|
31
35
|
return this.instantiateFactory(ctx, record, args)
|
|
32
36
|
default:
|
|
33
|
-
throw
|
|
34
|
-
`[
|
|
37
|
+
throw DIError.unknown(
|
|
38
|
+
`[Instantiator] Unknown service type: ${record.type}`,
|
|
35
39
|
)
|
|
36
40
|
}
|
|
37
41
|
} catch (error) {
|
|
38
|
-
return [error instanceof
|
|
42
|
+
return [error instanceof DIError ? error : DIError.unknown(String(error))]
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -50,7 +54,7 @@ export class ServiceInstantiator {
|
|
|
50
54
|
ctx: FactoryContext,
|
|
51
55
|
record: FactoryRecord<T, any>,
|
|
52
56
|
args: any,
|
|
53
|
-
): Promise<[undefined, T] | [
|
|
57
|
+
): Promise<[undefined, T] | [DIError]> {
|
|
54
58
|
try {
|
|
55
59
|
const tryLoad = this.injectors.wrapSyncInit(() => {
|
|
56
60
|
const original = this.injectors.provideFactoryContext(ctx)
|
|
@@ -63,8 +67,8 @@ export class ServiceInstantiator {
|
|
|
63
67
|
if (promises.length > 0) {
|
|
64
68
|
const results = await Promise.allSettled(promises)
|
|
65
69
|
if (results.some((result) => result.status === 'rejected')) {
|
|
66
|
-
throw
|
|
67
|
-
`[
|
|
70
|
+
throw DIError.unknown(
|
|
71
|
+
`[Instantiator] Service ${record.target.name} cannot be instantiated.`,
|
|
68
72
|
)
|
|
69
73
|
}
|
|
70
74
|
const newRes = tryLoad(injectState)
|
|
@@ -73,13 +77,13 @@ export class ServiceInstantiator {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
if (promises.length > 0) {
|
|
76
|
-
console.error(`[
|
|
80
|
+
console.error(`[Instantiator] ${record.target.name} has problem with it's definition.
|
|
77
81
|
|
|
78
82
|
One or more of the dependencies are registered as a InjectableScope.Instance and are used with inject.
|
|
79
83
|
|
|
80
84
|
Please use inject asyncInject of inject to load those dependencies.`)
|
|
81
|
-
throw
|
|
82
|
-
`[
|
|
85
|
+
throw DIError.unknown(
|
|
86
|
+
`[Instantiator] Service ${record.target.name} cannot be instantiated.`,
|
|
83
87
|
)
|
|
84
88
|
}
|
|
85
89
|
|
|
@@ -95,7 +99,7 @@ export class ServiceInstantiator {
|
|
|
95
99
|
|
|
96
100
|
return [undefined, instance]
|
|
97
101
|
} catch (error) {
|
|
98
|
-
return [error instanceof
|
|
102
|
+
return [error instanceof DIError ? error : DIError.unknown(String(error))]
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -110,7 +114,7 @@ export class ServiceInstantiator {
|
|
|
110
114
|
ctx: FactoryContext,
|
|
111
115
|
record: FactoryRecord<T, any>,
|
|
112
116
|
args: any,
|
|
113
|
-
): Promise<[undefined, T] | [
|
|
117
|
+
): Promise<[undefined, T] | [DIError]> {
|
|
114
118
|
try {
|
|
115
119
|
const tryLoad = this.injectors.wrapSyncInit(() => {
|
|
116
120
|
const original = this.injectors.provideFactoryContext(ctx)
|
|
@@ -123,8 +127,8 @@ export class ServiceInstantiator {
|
|
|
123
127
|
if (promises.length > 0) {
|
|
124
128
|
const results = await Promise.allSettled(promises)
|
|
125
129
|
if (results.some((result) => result.status === 'rejected')) {
|
|
126
|
-
throw
|
|
127
|
-
`[
|
|
130
|
+
throw DIError.unknown(
|
|
131
|
+
`[Instantiator] Service ${record.target.name} cannot be instantiated.`,
|
|
128
132
|
)
|
|
129
133
|
}
|
|
130
134
|
const newRes = tryLoad(injectState)
|
|
@@ -133,26 +137,26 @@ export class ServiceInstantiator {
|
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
if (promises.length > 0) {
|
|
136
|
-
console.error(`[
|
|
140
|
+
console.error(`[Instantiator] ${record.target.name} has problem with it's definition.
|
|
137
141
|
|
|
138
142
|
One or more of the dependencies are registered as a InjectableScope.Instance and are used with inject.
|
|
139
143
|
|
|
140
144
|
Please use asyncInject instead of inject to load those dependencies.`)
|
|
141
|
-
throw
|
|
142
|
-
`[
|
|
145
|
+
throw DIError.unknown(
|
|
146
|
+
`[Instantiator] Service ${record.target.name} cannot be instantiated.`,
|
|
143
147
|
)
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
if (typeof builder.create !== 'function') {
|
|
147
|
-
throw
|
|
148
|
-
`[
|
|
151
|
+
throw DIError.unknown(
|
|
152
|
+
`[Instantiator] Factory ${record.target.name} does not implement the create method.`,
|
|
149
153
|
)
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
const instance = await builder.create(ctx, args)
|
|
153
157
|
return [undefined, instance]
|
|
154
158
|
} catch (error) {
|
|
155
|
-
return [error instanceof
|
|
159
|
+
return [error instanceof DIError ? error : DIError.unknown(String(error))]
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
}
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { IHolderStorage } from '../holder/holder-storage.interface.mjs'
|
|
3
|
+
import type { LifecycleEventBus } from '../lifecycle/lifecycle-event-bus.mjs'
|
|
4
|
+
import type { InstanceHolder } from '../holder/instance-holder.mjs'
|
|
5
|
+
import type { HolderManager } from '../holder/holder-manager.mjs'
|
|
6
|
+
|
|
7
|
+
import { InstanceStatus } from '../holder/instance-holder.mjs'
|
|
8
|
+
import { SingletonStorage } from '../holder/singleton-storage.mjs'
|
|
9
|
+
|
|
10
|
+
export interface ClearAllOptions {
|
|
11
|
+
/** Maximum number of invalidation rounds to prevent infinite loops (default: 10) */
|
|
12
|
+
maxRounds?: number
|
|
13
|
+
/** Whether to wait for all services to settle before starting (default: true) */
|
|
14
|
+
waitForSettlement?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InvalidationOptions {
|
|
18
|
+
/** Whether to emit events after invalidation (default: true for singletons) */
|
|
19
|
+
emitEvents?: boolean
|
|
20
|
+
/** Custom event emitter function */
|
|
21
|
+
onInvalidated?: (instanceName: string) => Promise<void>
|
|
22
|
+
/** Whether to cascade invalidation to dependents (default: true) */
|
|
23
|
+
cascade?: boolean
|
|
24
|
+
/** Internal: tracks services being invalidated in the current call chain to prevent circular loops */
|
|
25
|
+
_invalidating?: Set<string>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Manages graceful service cleanup with dependency-aware invalidation.
|
|
30
|
+
*
|
|
31
|
+
* Ensures services are destroyed in the correct order based on their dependencies.
|
|
32
|
+
* Works with any IHolderStorage implementation, enabling unified invalidation
|
|
33
|
+
* for both singleton and request-scoped services.
|
|
34
|
+
*/
|
|
35
|
+
export class Invalidator {
|
|
36
|
+
private readonly storage: IHolderStorage
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
manager: HolderManager,
|
|
40
|
+
private readonly eventBus: LifecycleEventBus | null,
|
|
41
|
+
private readonly logger: Console | null = null,
|
|
42
|
+
) {
|
|
43
|
+
this.storage = new SingletonStorage(manager)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Invalidates a service and all its dependencies.
|
|
48
|
+
* Works with the configured storage (singleton by default).
|
|
49
|
+
*/
|
|
50
|
+
invalidate(service: string, round = 1): Promise<any> {
|
|
51
|
+
return this.invalidateWithStorage(service, this.storage, round)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Invalidates a service using a specific storage.
|
|
56
|
+
* This allows request-scoped invalidation using a RequestStorage.
|
|
57
|
+
*
|
|
58
|
+
* @param service The instance name to invalidate
|
|
59
|
+
* @param storage The storage to use for this invalidation
|
|
60
|
+
* @param round Current invalidation round (for recursion limiting)
|
|
61
|
+
* @param options Additional options for invalidation behavior
|
|
62
|
+
*/
|
|
63
|
+
async invalidateWithStorage(
|
|
64
|
+
service: string,
|
|
65
|
+
storage: IHolderStorage,
|
|
66
|
+
round = 1,
|
|
67
|
+
options: InvalidationOptions = {},
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const { cascade = true, _invalidating = new Set<string>() } = options
|
|
70
|
+
|
|
71
|
+
// Prevent infinite recursion from circular dependencies
|
|
72
|
+
if (_invalidating.has(service)) {
|
|
73
|
+
this.logger?.log(
|
|
74
|
+
`[Invalidator] Skipping ${service} - already being invalidated in this chain`,
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.logger?.log(
|
|
80
|
+
`[Invalidator] Starting invalidation process for ${service}`,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const result = storage.get(service)
|
|
84
|
+
if (result === null) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Mark this service as being invalidated
|
|
89
|
+
_invalidating.add(service)
|
|
90
|
+
|
|
91
|
+
// Pass the tracking set to cascaded invalidations
|
|
92
|
+
const optionsWithTracking = { ...options, _invalidating }
|
|
93
|
+
|
|
94
|
+
// Cascade invalidation: first invalidate all services that depend on this one
|
|
95
|
+
if (cascade) {
|
|
96
|
+
const dependents = storage.findDependents(service)
|
|
97
|
+
for (const dependentName of dependents) {
|
|
98
|
+
await this.invalidateWithStorage(dependentName, storage, round, optionsWithTracking)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const [, holder] = result
|
|
103
|
+
if (holder) {
|
|
104
|
+
await this.invalidateHolderWithStorage(service, holder, storage, round, optionsWithTracking)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Gracefully clears all services using invalidation logic.
|
|
110
|
+
* This method respects service dependencies and ensures proper cleanup order.
|
|
111
|
+
* Services that depend on others will be invalidated first, then their dependencies.
|
|
112
|
+
*/
|
|
113
|
+
async clearAll(options: ClearAllOptions = {}): Promise<void> {
|
|
114
|
+
return this.clearAllWithStorage(this.storage, options)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gracefully clears all services in a specific storage.
|
|
119
|
+
* This allows clearing request-scoped services using a RequestStorage.
|
|
120
|
+
*/
|
|
121
|
+
async clearAllWithStorage(
|
|
122
|
+
storage: IHolderStorage,
|
|
123
|
+
options: ClearAllOptions = {},
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const { maxRounds = 10, waitForSettlement = true } = options
|
|
126
|
+
|
|
127
|
+
this.logger?.log(
|
|
128
|
+
'[Invalidator] Starting graceful clearing of all services',
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
// Wait for all services to settle if requested
|
|
132
|
+
if (waitForSettlement) {
|
|
133
|
+
this.logger?.log(
|
|
134
|
+
'[Invalidator] Waiting for all services to settle...',
|
|
135
|
+
)
|
|
136
|
+
await this.readyWithStorage(storage)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Get all service names that need to be cleared
|
|
140
|
+
const allServiceNames = storage.getAllNames()
|
|
141
|
+
|
|
142
|
+
if (allServiceNames.length === 0) {
|
|
143
|
+
this.logger?.log('[Invalidator] No services to clear')
|
|
144
|
+
} else {
|
|
145
|
+
this.logger?.log(
|
|
146
|
+
`[Invalidator] Found ${allServiceNames.length} services to clear: ${allServiceNames.join(', ')}`,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Clear services using dependency-aware invalidation
|
|
150
|
+
await this.clearServicesWithDependencyAwarenessForStorage(
|
|
151
|
+
allServiceNames,
|
|
152
|
+
maxRounds,
|
|
153
|
+
storage,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.logger?.log('[Invalidator] Graceful clearing completed')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Waits for all services to settle (either created, destroyed, or error state).
|
|
162
|
+
*/
|
|
163
|
+
async ready(): Promise<void> {
|
|
164
|
+
return this.readyWithStorage(this.storage)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Waits for all services in a specific storage to settle.
|
|
169
|
+
*/
|
|
170
|
+
async readyWithStorage(storage: IHolderStorage): Promise<void> {
|
|
171
|
+
const holders: InstanceHolder<any>[] = []
|
|
172
|
+
storage.forEach((_: string, holder: InstanceHolder) => holders.push(holder))
|
|
173
|
+
await Promise.all(
|
|
174
|
+
holders.map((holder) => this.waitForHolderToSettle(holder)),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// INTERNAL INVALIDATION HELPERS
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Invalidates a single holder using a specific storage.
|
|
184
|
+
*/
|
|
185
|
+
private async invalidateHolderWithStorage(
|
|
186
|
+
key: string,
|
|
187
|
+
holder: InstanceHolder<any>,
|
|
188
|
+
storage: IHolderStorage,
|
|
189
|
+
round: number,
|
|
190
|
+
options: InvalidationOptions = {},
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const { emitEvents = true, onInvalidated } = options
|
|
193
|
+
|
|
194
|
+
await this.invalidateHolderByStatus(holder, round, {
|
|
195
|
+
context: key,
|
|
196
|
+
onCreationError: () =>
|
|
197
|
+
this.logger?.error(
|
|
198
|
+
`[Invalidator] ${key} creation triggered too many invalidation rounds`,
|
|
199
|
+
),
|
|
200
|
+
onRecursiveInvalidate: () =>
|
|
201
|
+
this.invalidateWithStorage(key, storage, round + 1, options),
|
|
202
|
+
onDestroy: () =>
|
|
203
|
+
this.destroyHolderWithStorage(key, holder, storage, emitEvents, onInvalidated),
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Common invalidation logic for holders based on their status.
|
|
209
|
+
*/
|
|
210
|
+
private async invalidateHolderByStatus(
|
|
211
|
+
holder: InstanceHolder<any>,
|
|
212
|
+
round: number,
|
|
213
|
+
options: {
|
|
214
|
+
context: string
|
|
215
|
+
onCreationError: () => void
|
|
216
|
+
onRecursiveInvalidate: () => Promise<void>
|
|
217
|
+
onDestroy: () => Promise<void>
|
|
218
|
+
},
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
switch (holder.status) {
|
|
221
|
+
case InstanceStatus.Destroying:
|
|
222
|
+
await holder.destroyPromise
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
case InstanceStatus.Creating:
|
|
226
|
+
await holder.creationPromise
|
|
227
|
+
if (round > 3) {
|
|
228
|
+
options.onCreationError()
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
await options.onRecursiveInvalidate()
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
default:
|
|
235
|
+
await options.onDestroy()
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Destroys a holder using a specific storage.
|
|
242
|
+
*/
|
|
243
|
+
private async destroyHolderWithStorage(
|
|
244
|
+
key: string,
|
|
245
|
+
holder: InstanceHolder<any>,
|
|
246
|
+
storage: IHolderStorage,
|
|
247
|
+
emitEvents: boolean,
|
|
248
|
+
onInvalidated?: (instanceName: string) => Promise<void>,
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
holder.status = InstanceStatus.Destroying
|
|
251
|
+
this.logger?.log(`[Invalidator] Invalidating ${key} and notifying listeners`)
|
|
252
|
+
|
|
253
|
+
holder.destroyPromise = Promise.all(
|
|
254
|
+
holder.destroyListeners.map((listener) => listener()),
|
|
255
|
+
).then(async () => {
|
|
256
|
+
holder.destroyListeners = []
|
|
257
|
+
holder.deps.clear()
|
|
258
|
+
storage.delete(key)
|
|
259
|
+
|
|
260
|
+
// Emit events if enabled and event bus exists
|
|
261
|
+
if (emitEvents && this.eventBus) {
|
|
262
|
+
await this.emitInstanceEvent(key, 'destroy')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Call custom callback if provided
|
|
266
|
+
if (onInvalidated) {
|
|
267
|
+
await onInvalidated(key)
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
await holder.destroyPromise
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Waits for a holder to settle (either created, destroyed, or error state).
|
|
276
|
+
*/
|
|
277
|
+
private async waitForHolderToSettle(
|
|
278
|
+
holder: InstanceHolder<any>,
|
|
279
|
+
): Promise<void> {
|
|
280
|
+
switch (holder.status) {
|
|
281
|
+
case InstanceStatus.Creating:
|
|
282
|
+
await holder.creationPromise
|
|
283
|
+
break
|
|
284
|
+
case InstanceStatus.Destroying:
|
|
285
|
+
await holder.destroyPromise
|
|
286
|
+
break
|
|
287
|
+
// Already settled states
|
|
288
|
+
case InstanceStatus.Created:
|
|
289
|
+
case InstanceStatus.Error:
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Clears services with dependency awareness for a specific storage.
|
|
296
|
+
*/
|
|
297
|
+
private async clearServicesWithDependencyAwarenessForStorage(
|
|
298
|
+
serviceNames: string[],
|
|
299
|
+
maxRounds: number,
|
|
300
|
+
storage: IHolderStorage,
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const clearedServices = new Set<string>()
|
|
303
|
+
let round = 1
|
|
304
|
+
|
|
305
|
+
while (clearedServices.size < serviceNames.length && round <= maxRounds) {
|
|
306
|
+
this.logger?.log(
|
|
307
|
+
`[Invalidator] Clearing round ${round}/${maxRounds}, ${clearedServices.size}/${serviceNames.length} services cleared`,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// Find services that can be cleared in this round
|
|
311
|
+
const servicesToClearThisRound = this.findServicesReadyForClearingInStorage(
|
|
312
|
+
serviceNames,
|
|
313
|
+
clearedServices,
|
|
314
|
+
storage,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if (servicesToClearThisRound.length === 0) {
|
|
318
|
+
// If no services can be cleared, try to clear remaining services anyway
|
|
319
|
+
// This handles circular dependencies or other edge cases
|
|
320
|
+
const remainingServices = serviceNames.filter(
|
|
321
|
+
(name) => !clearedServices.has(name),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if (remainingServices.length > 0) {
|
|
325
|
+
this.logger?.warn(
|
|
326
|
+
`[Invalidator] No services ready for clearing, forcing cleanup of remaining: ${remainingServices.join(', ')}`,
|
|
327
|
+
)
|
|
328
|
+
await this.forceClearServicesInStorage(remainingServices, storage)
|
|
329
|
+
remainingServices.forEach((name) => clearedServices.add(name))
|
|
330
|
+
}
|
|
331
|
+
break
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Clear services in this round
|
|
335
|
+
const clearPromises = servicesToClearThisRound.map(
|
|
336
|
+
async (serviceName) => {
|
|
337
|
+
try {
|
|
338
|
+
await this.invalidateWithStorage(serviceName, storage, round)
|
|
339
|
+
clearedServices.add(serviceName)
|
|
340
|
+
this.logger?.log(
|
|
341
|
+
`[Invalidator] Successfully cleared service: ${serviceName}`,
|
|
342
|
+
)
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.logger?.error(
|
|
345
|
+
`[Invalidator] Error clearing service ${serviceName}:`,
|
|
346
|
+
error,
|
|
347
|
+
)
|
|
348
|
+
// Still mark as cleared to avoid infinite loops
|
|
349
|
+
clearedServices.add(serviceName)
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
await Promise.all(clearPromises)
|
|
355
|
+
round++
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (clearedServices.size < serviceNames.length) {
|
|
359
|
+
this.logger?.warn(
|
|
360
|
+
`[Invalidator] Clearing completed after ${maxRounds} rounds, but ${serviceNames.length - clearedServices.size} services may not have been properly cleared`,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Finds services that are ready to be cleared in the current round.
|
|
367
|
+
* A service is ready if all its dependencies have already been cleared.
|
|
368
|
+
*/
|
|
369
|
+
private findServicesReadyForClearingInStorage(
|
|
370
|
+
allServiceNames: string[],
|
|
371
|
+
clearedServices: Set<string>,
|
|
372
|
+
storage: IHolderStorage,
|
|
373
|
+
): string[] {
|
|
374
|
+
return allServiceNames.filter((serviceName) => {
|
|
375
|
+
if (clearedServices.has(serviceName)) {
|
|
376
|
+
return false // Already cleared
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check if this service has any dependencies that haven't been cleared yet
|
|
380
|
+
const result = storage.get(serviceName)
|
|
381
|
+
if (result === null || result[0]) {
|
|
382
|
+
return true // Service not found or in error state, can be cleared
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const [, holder] = result
|
|
386
|
+
// Check if all dependencies have been cleared
|
|
387
|
+
const hasUnclearedDependencies = Array.from(holder!.deps).some(
|
|
388
|
+
(dep) => !clearedServices.has(dep),
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return !hasUnclearedDependencies
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Force clears services that couldn't be cleared through normal dependency resolution.
|
|
397
|
+
* This handles edge cases like circular dependencies.
|
|
398
|
+
*/
|
|
399
|
+
private async forceClearServicesInStorage(
|
|
400
|
+
serviceNames: string[],
|
|
401
|
+
storage: IHolderStorage,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
const promises = serviceNames.map(async (serviceName) => {
|
|
404
|
+
try {
|
|
405
|
+
// Directly destroy the holder without going through normal invalidation
|
|
406
|
+
const result = storage.get(serviceName)
|
|
407
|
+
if (result !== null && !result[0]) {
|
|
408
|
+
const [, holder] = result
|
|
409
|
+
await this.destroyHolderWithStorage(serviceName, holder!, storage, true)
|
|
410
|
+
}
|
|
411
|
+
} catch (error) {
|
|
412
|
+
this.logger?.error(
|
|
413
|
+
`[Invalidator] Error force clearing service ${serviceName}:`,
|
|
414
|
+
error,
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
await Promise.all(promises)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Emits events to listeners for instance lifecycle events.
|
|
424
|
+
*/
|
|
425
|
+
private emitInstanceEvent(
|
|
426
|
+
name: string,
|
|
427
|
+
event: 'create' | 'destroy' = 'create',
|
|
428
|
+
) {
|
|
429
|
+
if (!this.eventBus) {
|
|
430
|
+
return Promise.resolve()
|
|
431
|
+
}
|
|
432
|
+
this.logger?.log(
|
|
433
|
+
`[Invalidator]#emitInstanceEvent() Notifying listeners for ${name} with event ${event}`,
|
|
434
|
+
)
|
|
435
|
+
return this.eventBus.emit(name, event)
|
|
436
|
+
}
|
|
437
|
+
}
|