@navios/di 0.4.1 → 0.5.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/README.md +211 -1
- package/coverage/clover.xml +1912 -1277
- package/coverage/coverage-final.json +37 -28
- package/coverage/docs/examples/basic-usage.mts.html +1 -1
- package/coverage/docs/examples/factory-pattern.mts.html +1 -1
- package/coverage/docs/examples/index.html +1 -1
- package/coverage/docs/examples/injection-tokens.mts.html +1 -1
- package/coverage/docs/examples/request-scope-example.mts.html +1 -1
- package/coverage/docs/examples/service-lifecycle.mts.html +1 -1
- package/coverage/index.html +71 -41
- package/coverage/lib/_tsup-dts-rollup.d.mts.html +682 -43
- package/coverage/lib/index.d.mts.html +7 -4
- package/coverage/lib/index.html +5 -5
- package/coverage/lib/testing/index.d.mts.html +91 -0
- package/coverage/lib/testing/index.html +116 -0
- package/coverage/src/base-instance-holder-manager.mts.html +589 -0
- package/coverage/src/container.mts.html +257 -74
- package/coverage/src/decorators/factory.decorator.mts.html +1 -1
- package/coverage/src/decorators/index.html +1 -1
- package/coverage/src/decorators/index.mts.html +1 -1
- package/coverage/src/decorators/injectable.decorator.mts.html +20 -20
- package/coverage/src/enums/index.html +1 -1
- package/coverage/src/enums/index.mts.html +1 -1
- package/coverage/src/enums/injectable-scope.enum.mts.html +1 -1
- package/coverage/src/enums/injectable-type.enum.mts.html +1 -1
- package/coverage/src/errors/di-error.mts.html +292 -0
- package/coverage/src/errors/errors.enum.mts.html +30 -21
- package/coverage/src/errors/factory-not-found.mts.html +31 -22
- package/coverage/src/errors/factory-token-not-resolved.mts.html +29 -26
- package/coverage/src/errors/index.html +56 -41
- package/coverage/src/errors/index.mts.html +15 -9
- package/coverage/src/errors/instance-destroying.mts.html +31 -22
- package/coverage/src/errors/instance-expired.mts.html +31 -22
- package/coverage/src/errors/instance-not-found.mts.html +31 -22
- package/coverage/src/errors/unknown-error.mts.html +31 -43
- package/coverage/src/event-emitter.mts.html +14 -14
- package/coverage/src/factory-context.mts.html +1 -1
- package/coverage/src/index.html +121 -46
- package/coverage/src/index.mts.html +7 -4
- package/coverage/src/injection-token.mts.html +28 -28
- package/coverage/src/injector.mts.html +1 -1
- package/coverage/src/instance-resolver.mts.html +1762 -0
- package/coverage/src/interfaces/factory.interface.mts.html +1 -1
- package/coverage/src/interfaces/index.html +1 -1
- package/coverage/src/interfaces/index.mts.html +1 -1
- package/coverage/src/interfaces/on-service-destroy.interface.mts.html +1 -1
- package/coverage/src/interfaces/on-service-init.interface.mts.html +1 -1
- package/coverage/src/registry.mts.html +28 -28
- package/coverage/src/request-context-holder.mts.html +183 -102
- package/coverage/src/request-context-manager.mts.html +532 -0
- package/coverage/src/service-instantiator.mts.html +49 -49
- package/coverage/src/service-invalidator.mts.html +1372 -0
- package/coverage/src/service-locator-event-bus.mts.html +48 -48
- package/coverage/src/service-locator-instance-holder.mts.html +2 -14
- package/coverage/src/service-locator-manager.mts.html +71 -335
- package/coverage/src/service-locator.mts.html +240 -2328
- package/coverage/src/symbols/index.html +1 -1
- package/coverage/src/symbols/index.mts.html +1 -1
- package/coverage/src/symbols/injectable-token.mts.html +1 -1
- package/coverage/src/testing/index.html +131 -0
- package/coverage/src/testing/index.mts.html +88 -0
- package/coverage/src/testing/test-container.mts.html +445 -0
- package/coverage/src/token-processor.mts.html +607 -0
- package/coverage/src/utils/defer.mts.html +28 -214
- package/coverage/src/utils/get-injectable-token.mts.html +7 -7
- package/coverage/src/utils/get-injectors.mts.html +99 -99
- package/coverage/src/utils/index.html +15 -15
- package/coverage/src/utils/index.mts.html +4 -7
- package/coverage/src/utils/types.mts.html +1 -1
- package/docs/injectable.md +51 -11
- package/docs/scopes.md +63 -29
- package/lib/_tsup-dts-rollup.d.mts +447 -212
- package/lib/_tsup-dts-rollup.d.ts +447 -212
- package/lib/chunk-44F3LXW5.mjs +2043 -0
- package/lib/chunk-44F3LXW5.mjs.map +1 -0
- package/lib/index.d.mts +6 -4
- package/lib/index.d.ts +6 -4
- package/lib/index.js +1199 -773
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +4 -1599
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.d.mts +2 -0
- package/lib/testing/index.d.ts +2 -0
- package/lib/testing/index.js +2060 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/index.mjs +73 -0
- package/lib/testing/index.mjs.map +1 -0
- package/package.json +11 -1
- package/src/__tests__/container.spec.mts +47 -13
- package/src/__tests__/errors.spec.mts +53 -27
- package/src/__tests__/injectable.spec.mts +73 -0
- package/src/__tests__/request-scope.spec.mts +0 -2
- package/src/__tests__/service-locator-manager.spec.mts +12 -82
- package/src/__tests__/service-locator.spec.mts +1009 -1
- package/src/__type-tests__/inject.spec-d.mts +30 -7
- package/src/__type-tests__/injectable.spec-d.mts +76 -37
- package/src/base-instance-holder-manager.mts +2 -9
- package/src/container.mts +70 -10
- package/src/decorators/injectable.decorator.mts +29 -5
- package/src/errors/di-error.mts +69 -0
- package/src/errors/index.mts +9 -7
- package/src/injection-token.mts +1 -0
- package/src/injector.mts +2 -0
- package/src/instance-resolver.mts +559 -0
- package/src/request-context-holder.mts +0 -2
- package/src/request-context-manager.mts +149 -0
- package/src/service-invalidator.mts +429 -0
- package/src/service-locator-instance-holder.mts +0 -4
- package/src/service-locator-manager.mts +10 -40
- package/src/service-locator.mts +86 -782
- package/src/testing/README.md +80 -0
- package/src/testing/__tests__/test-container.spec.mts +173 -0
- package/src/testing/index.mts +1 -0
- package/src/testing/test-container.mts +120 -0
- package/src/token-processor.mts +174 -0
- package/src/utils/get-injectors.mts +161 -24
- package/src/utils/index.mts +0 -1
- package/src/utils/types.mts +12 -8
- package/tsup.config.mts +1 -1
- package/src/__tests__/defer.spec.mts +0 -166
- package/src/errors/errors.enum.mts +0 -8
- package/src/errors/factory-not-found.mts +0 -8
- package/src/errors/factory-token-not-resolved.mts +0 -10
- package/src/errors/instance-destroying.mts +0 -8
- package/src/errors/instance-expired.mts +0 -8
- package/src/errors/instance-not-found.mts +0 -8
- package/src/errors/unknown-error.mts +0 -15
- package/src/utils/defer.mts +0 -73
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { RequestContextHolder } from './request-context-holder.mjs'
|
|
2
|
+
|
|
3
|
+
import { DefaultRequestContextHolder } from './request-context-holder.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RequestContextManager handles request context lifecycle management.
|
|
7
|
+
* Extracted from ServiceLocator to improve separation of concerns.
|
|
8
|
+
*/
|
|
9
|
+
export class RequestContextManager {
|
|
10
|
+
private readonly requestContexts = new Map<string, RequestContextHolder>()
|
|
11
|
+
private currentRequestContext: RequestContextHolder | null = null
|
|
12
|
+
|
|
13
|
+
constructor(private readonly logger: Console | null = null) {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Begins a new request context with the given parameters.
|
|
17
|
+
* @param requestId Unique identifier for this request
|
|
18
|
+
* @param metadata Optional metadata for the request
|
|
19
|
+
* @param priority Priority for resolution (higher = more priority)
|
|
20
|
+
* @returns The created request context holder
|
|
21
|
+
*/
|
|
22
|
+
beginRequest(
|
|
23
|
+
requestId: string,
|
|
24
|
+
metadata?: Record<string, any>,
|
|
25
|
+
priority: number = 100,
|
|
26
|
+
): RequestContextHolder {
|
|
27
|
+
if (this.requestContexts.has(requestId)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[RequestContextManager] Request context ${requestId} already exists`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const contextHolder = new DefaultRequestContextHolder(
|
|
34
|
+
requestId,
|
|
35
|
+
priority,
|
|
36
|
+
metadata,
|
|
37
|
+
)
|
|
38
|
+
this.requestContexts.set(requestId, contextHolder)
|
|
39
|
+
this.currentRequestContext = contextHolder
|
|
40
|
+
|
|
41
|
+
this.logger?.log(
|
|
42
|
+
`[RequestContextManager] Started request context: ${requestId}`,
|
|
43
|
+
)
|
|
44
|
+
return contextHolder
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ends a request context and cleans up all associated instances.
|
|
49
|
+
* @param requestId The request ID to end
|
|
50
|
+
*/
|
|
51
|
+
async endRequest(requestId: string): Promise<void> {
|
|
52
|
+
const contextHolder = this.requestContexts.get(requestId)
|
|
53
|
+
if (!contextHolder) {
|
|
54
|
+
this.logger?.warn(
|
|
55
|
+
`[RequestContextManager] Request context ${requestId} not found`,
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.logger?.log(
|
|
61
|
+
`[RequestContextManager] Ending request context: ${requestId}`,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// Clean up all request-scoped instances
|
|
65
|
+
const cleanupPromises: Promise<any>[] = []
|
|
66
|
+
for (const [, holder] of contextHolder.holders) {
|
|
67
|
+
if (holder.destroyListeners.length > 0) {
|
|
68
|
+
cleanupPromises.push(
|
|
69
|
+
Promise.all(holder.destroyListeners.map((listener) => listener())),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await Promise.all(cleanupPromises)
|
|
75
|
+
|
|
76
|
+
// Clear the context
|
|
77
|
+
contextHolder.clear()
|
|
78
|
+
this.requestContexts.delete(requestId)
|
|
79
|
+
|
|
80
|
+
// Reset current context if it was the one being ended
|
|
81
|
+
if (this.currentRequestContext === contextHolder) {
|
|
82
|
+
this.currentRequestContext =
|
|
83
|
+
Array.from(this.requestContexts.values()).at(-1) ?? null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.logger?.log(
|
|
87
|
+
`[RequestContextManager] Request context ${requestId} ended`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Gets the current request context.
|
|
93
|
+
* @returns The current request context holder or null
|
|
94
|
+
*/
|
|
95
|
+
getCurrentRequestContext(): RequestContextHolder | null {
|
|
96
|
+
return this.currentRequestContext
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sets the current request context.
|
|
101
|
+
* @param requestId The request ID to set as current
|
|
102
|
+
*/
|
|
103
|
+
setCurrentRequestContext(requestId: string): void {
|
|
104
|
+
const contextHolder = this.requestContexts.get(requestId)
|
|
105
|
+
if (!contextHolder) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`[RequestContextManager] Request context ${requestId} not found`,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
this.currentRequestContext = contextHolder
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gets all request contexts.
|
|
115
|
+
* @returns Map of request contexts
|
|
116
|
+
*/
|
|
117
|
+
getRequestContexts(): Map<string, RequestContextHolder> {
|
|
118
|
+
return this.requestContexts
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Clears all request contexts.
|
|
123
|
+
*/
|
|
124
|
+
async clearAllRequestContexts(): Promise<void> {
|
|
125
|
+
const requestIds = Array.from(this.requestContexts.keys())
|
|
126
|
+
|
|
127
|
+
if (requestIds.length === 0) {
|
|
128
|
+
this.logger?.log('[RequestContextManager] No request contexts to clear')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.logger?.log(
|
|
133
|
+
`[RequestContextManager] Clearing ${requestIds.length} request contexts: ${requestIds.join(', ')}`,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Clear request contexts sequentially to avoid race conditions
|
|
137
|
+
for (const requestId of requestIds) {
|
|
138
|
+
try {
|
|
139
|
+
await this.endRequest(requestId)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.logger?.error(
|
|
142
|
+
`[RequestContextManager] Error clearing request context ${requestId}:`,
|
|
143
|
+
error,
|
|
144
|
+
)
|
|
145
|
+
// Continue with other request contexts even if one fails
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { RequestContextManager } from './request-context-manager.mjs'
|
|
3
|
+
import type { ServiceLocatorEventBus } from './service-locator-event-bus.mjs'
|
|
4
|
+
import type { ServiceLocatorInstanceHolder } from './service-locator-instance-holder.mjs'
|
|
5
|
+
import type { ServiceLocatorManager } from './service-locator-manager.mjs'
|
|
6
|
+
|
|
7
|
+
import { ServiceLocatorInstanceHolderStatus } from './service-locator-instance-holder.mjs'
|
|
8
|
+
|
|
9
|
+
export interface ClearAllOptions {
|
|
10
|
+
/** Whether to also clear request contexts (default: true) */
|
|
11
|
+
clearRequestContexts?: boolean
|
|
12
|
+
/** Maximum number of invalidation rounds to prevent infinite loops (default: 10) */
|
|
13
|
+
maxRounds?: number
|
|
14
|
+
/** Whether to wait for all services to settle before starting (default: true) */
|
|
15
|
+
waitForSettlement?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ServiceInvalidator handles service invalidation, cleanup, and graceful clearing.
|
|
20
|
+
* Extracted from ServiceLocator to improve separation of concerns.
|
|
21
|
+
*/
|
|
22
|
+
export class ServiceInvalidator {
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly manager: ServiceLocatorManager,
|
|
25
|
+
private readonly requestContextManager: RequestContextManager,
|
|
26
|
+
private readonly eventBus: ServiceLocatorEventBus,
|
|
27
|
+
private readonly logger: Console | null = null,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Invalidates a service and all its dependencies.
|
|
32
|
+
*/
|
|
33
|
+
invalidate(service: string, round = 1): Promise<any> {
|
|
34
|
+
this.logger?.log(
|
|
35
|
+
`[ServiceInvalidator] Starting invalidation process for ${service}`,
|
|
36
|
+
)
|
|
37
|
+
const [, toInvalidate] = this.manager.get(service)
|
|
38
|
+
|
|
39
|
+
const promises = []
|
|
40
|
+
if (toInvalidate) {
|
|
41
|
+
promises.push(this.invalidateHolder(service, toInvalidate, round))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Also invalidate request-scoped instances that depend on the service or match the service name
|
|
45
|
+
const requestContexts = this.requestContextManager.getRequestContexts()
|
|
46
|
+
for (const [requestId, requestContext] of requestContexts.entries()) {
|
|
47
|
+
const holder = requestContext.get(service)
|
|
48
|
+
if (holder) {
|
|
49
|
+
this.logger?.log(
|
|
50
|
+
`[ServiceInvalidator] Invalidating request-scoped instance ${service} in request ${requestId}`,
|
|
51
|
+
)
|
|
52
|
+
promises.push(
|
|
53
|
+
this.invalidateRequestHolder(requestId, service, holder, round),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Promise.all(promises)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gracefully clears all services in the ServiceLocator using invalidation logic.
|
|
63
|
+
* This method respects service dependencies and ensures proper cleanup order.
|
|
64
|
+
* Services that depend on others will be invalidated first, then their dependencies.
|
|
65
|
+
*/
|
|
66
|
+
async clearAll(options: ClearAllOptions = {}): Promise<void> {
|
|
67
|
+
const {
|
|
68
|
+
clearRequestContexts = true,
|
|
69
|
+
maxRounds = 10,
|
|
70
|
+
waitForSettlement = true,
|
|
71
|
+
} = options
|
|
72
|
+
|
|
73
|
+
this.logger?.log(
|
|
74
|
+
'[ServiceInvalidator] Starting graceful clearing of all services',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Wait for all services to settle if requested
|
|
78
|
+
if (waitForSettlement) {
|
|
79
|
+
this.logger?.log(
|
|
80
|
+
'[ServiceInvalidator] Waiting for all services to settle...',
|
|
81
|
+
)
|
|
82
|
+
await this.ready()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get all service names that need to be cleared
|
|
86
|
+
const allServiceNames = this.getAllServiceNames()
|
|
87
|
+
|
|
88
|
+
if (allServiceNames.length === 0) {
|
|
89
|
+
this.logger?.log('[ServiceInvalidator] No singleton services to clear')
|
|
90
|
+
} else {
|
|
91
|
+
this.logger?.log(
|
|
92
|
+
`[ServiceInvalidator] Found ${allServiceNames.length} services to clear: ${allServiceNames.join(', ')}`,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// Clear services using dependency-aware invalidation
|
|
96
|
+
await this.clearServicesWithDependencyAwareness(
|
|
97
|
+
allServiceNames,
|
|
98
|
+
maxRounds,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Clear request contexts if requested
|
|
103
|
+
if (clearRequestContexts) {
|
|
104
|
+
await this.requestContextManager.clearAllRequestContexts()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.logger?.log('[ServiceInvalidator] Graceful clearing completed')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Waits for all services to settle (either created, destroyed, or error state).
|
|
112
|
+
*/
|
|
113
|
+
async ready(): Promise<void> {
|
|
114
|
+
const holders = Array.from(this.manager.filter(() => true)).map(
|
|
115
|
+
([, holder]) => holder,
|
|
116
|
+
)
|
|
117
|
+
await Promise.all(
|
|
118
|
+
holders.map((holder) => this.waitForHolderToSettle(holder)),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Invalidates a single holder based on its current status.
|
|
124
|
+
*/
|
|
125
|
+
private async invalidateHolder(
|
|
126
|
+
key: string,
|
|
127
|
+
holder: ServiceLocatorInstanceHolder<any>,
|
|
128
|
+
round: number,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
await this.invalidateHolderByStatus(holder, round, {
|
|
131
|
+
context: key,
|
|
132
|
+
isRequestScoped: false,
|
|
133
|
+
onCreationError: () =>
|
|
134
|
+
this.logger?.error(
|
|
135
|
+
`[ServiceInvalidator] ${key} creation triggered too many invalidation rounds`,
|
|
136
|
+
),
|
|
137
|
+
onRecursiveInvalidate: () => this.invalidate(key, round + 1),
|
|
138
|
+
onDestroy: () => this.destroyHolder(key, holder),
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Invalidates a request-scoped holder based on its current status.
|
|
144
|
+
*/
|
|
145
|
+
private async invalidateRequestHolder(
|
|
146
|
+
requestId: string,
|
|
147
|
+
instanceName: string,
|
|
148
|
+
holder: ServiceLocatorInstanceHolder<any>,
|
|
149
|
+
round: number,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
await this.invalidateHolderByStatus(holder, round, {
|
|
152
|
+
context: `Request-scoped ${instanceName} in ${requestId}`,
|
|
153
|
+
isRequestScoped: true,
|
|
154
|
+
onCreationError: () =>
|
|
155
|
+
this.logger?.error(
|
|
156
|
+
`[ServiceInvalidator] Request-scoped ${instanceName} in ${requestId} creation triggered too many invalidation rounds`,
|
|
157
|
+
),
|
|
158
|
+
onRecursiveInvalidate: () =>
|
|
159
|
+
this.invalidateRequestHolder(
|
|
160
|
+
requestId,
|
|
161
|
+
instanceName,
|
|
162
|
+
holder,
|
|
163
|
+
round + 1,
|
|
164
|
+
),
|
|
165
|
+
onDestroy: () =>
|
|
166
|
+
this.destroyRequestHolder(requestId, instanceName, holder),
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Common invalidation logic for holders based on their status.
|
|
172
|
+
*/
|
|
173
|
+
private async invalidateHolderByStatus(
|
|
174
|
+
holder: ServiceLocatorInstanceHolder<any>,
|
|
175
|
+
round: number,
|
|
176
|
+
options: {
|
|
177
|
+
context: string
|
|
178
|
+
isRequestScoped: boolean
|
|
179
|
+
onCreationError: () => void
|
|
180
|
+
onRecursiveInvalidate: () => Promise<void>
|
|
181
|
+
onDestroy: () => Promise<void>
|
|
182
|
+
},
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
switch (holder.status) {
|
|
185
|
+
case ServiceLocatorInstanceHolderStatus.Destroying:
|
|
186
|
+
await holder.destroyPromise
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
case ServiceLocatorInstanceHolderStatus.Creating:
|
|
190
|
+
await holder.creationPromise
|
|
191
|
+
if (round > 3) {
|
|
192
|
+
options.onCreationError()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
await options.onRecursiveInvalidate()
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
default:
|
|
199
|
+
await options.onDestroy()
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Destroys a holder and cleans up its resources.
|
|
206
|
+
*/
|
|
207
|
+
private async destroyHolder(
|
|
208
|
+
key: string,
|
|
209
|
+
holder: ServiceLocatorInstanceHolder<any>,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
await this.destroyHolderWithCleanup(holder, {
|
|
212
|
+
context: key,
|
|
213
|
+
logMessage: `[ServiceInvalidator] Invalidating ${key} and notifying listeners`,
|
|
214
|
+
cleanup: () => this.manager.delete(key),
|
|
215
|
+
eventName: key,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Destroys a request-scoped holder and cleans up its resources.
|
|
221
|
+
*/
|
|
222
|
+
private async destroyRequestHolder(
|
|
223
|
+
requestId: string,
|
|
224
|
+
instanceName: string,
|
|
225
|
+
holder: ServiceLocatorInstanceHolder<any>,
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
await this.destroyHolderWithCleanup(holder, {
|
|
228
|
+
context: `Request-scoped ${instanceName} in ${requestId}`,
|
|
229
|
+
logMessage: `[ServiceInvalidator] Invalidating request-scoped ${instanceName} in ${requestId} and notifying listeners`,
|
|
230
|
+
cleanup: () => {
|
|
231
|
+
const requestContext = this.requestContextManager
|
|
232
|
+
.getRequestContexts()
|
|
233
|
+
.get(requestId)
|
|
234
|
+
if (requestContext) {
|
|
235
|
+
requestContext.delete(instanceName)
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
eventName: instanceName,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Common destroy logic for holders with customizable cleanup.
|
|
244
|
+
*/
|
|
245
|
+
private async destroyHolderWithCleanup(
|
|
246
|
+
holder: ServiceLocatorInstanceHolder<any>,
|
|
247
|
+
options: {
|
|
248
|
+
context: string
|
|
249
|
+
logMessage: string
|
|
250
|
+
cleanup: () => void
|
|
251
|
+
eventName: string
|
|
252
|
+
},
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
holder.status = ServiceLocatorInstanceHolderStatus.Destroying
|
|
255
|
+
this.logger?.log(options.logMessage)
|
|
256
|
+
|
|
257
|
+
holder.destroyPromise = Promise.all(
|
|
258
|
+
holder.destroyListeners.map((listener) => listener()),
|
|
259
|
+
).then(async () => {
|
|
260
|
+
holder.destroyListeners = []
|
|
261
|
+
holder.deps.clear()
|
|
262
|
+
options.cleanup()
|
|
263
|
+
await this.emitInstanceEvent(options.eventName, 'destroy')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await holder.destroyPromise
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Waits for a holder to settle (either created, destroyed, or error state).
|
|
271
|
+
*/
|
|
272
|
+
private async waitForHolderToSettle(
|
|
273
|
+
holder: ServiceLocatorInstanceHolder<any>,
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
switch (holder.status) {
|
|
276
|
+
case ServiceLocatorInstanceHolderStatus.Creating:
|
|
277
|
+
await holder.creationPromise
|
|
278
|
+
break
|
|
279
|
+
case ServiceLocatorInstanceHolderStatus.Destroying:
|
|
280
|
+
await holder.destroyPromise
|
|
281
|
+
break
|
|
282
|
+
// Already settled states
|
|
283
|
+
case ServiceLocatorInstanceHolderStatus.Created:
|
|
284
|
+
case ServiceLocatorInstanceHolderStatus.Error:
|
|
285
|
+
break
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Clears services with dependency awareness, ensuring proper cleanup order.
|
|
291
|
+
* Services with no dependencies are cleared first, then services that depend on them.
|
|
292
|
+
*/
|
|
293
|
+
private async clearServicesWithDependencyAwareness(
|
|
294
|
+
serviceNames: string[],
|
|
295
|
+
maxRounds: number,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
const clearedServices = new Set<string>()
|
|
298
|
+
let round = 1
|
|
299
|
+
|
|
300
|
+
while (clearedServices.size < serviceNames.length && round <= maxRounds) {
|
|
301
|
+
this.logger?.log(
|
|
302
|
+
`[ServiceInvalidator] Clearing round ${round}/${maxRounds}, ${clearedServices.size}/${serviceNames.length} services cleared`,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Find services that can be cleared in this round
|
|
306
|
+
const servicesToClearThisRound = this.findServicesReadyForClearing(
|
|
307
|
+
serviceNames,
|
|
308
|
+
clearedServices,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if (servicesToClearThisRound.length === 0) {
|
|
312
|
+
// If no services can be cleared, try to clear remaining services anyway
|
|
313
|
+
// This handles circular dependencies or other edge cases
|
|
314
|
+
const remainingServices = serviceNames.filter(
|
|
315
|
+
(name) => !clearedServices.has(name),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if (remainingServices.length > 0) {
|
|
319
|
+
this.logger?.warn(
|
|
320
|
+
`[ServiceInvalidator] No services ready for clearing, forcing cleanup of remaining: ${remainingServices.join(', ')}`,
|
|
321
|
+
)
|
|
322
|
+
await this.forceClearServices(remainingServices)
|
|
323
|
+
remainingServices.forEach((name) => clearedServices.add(name))
|
|
324
|
+
}
|
|
325
|
+
break
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Clear services in this round
|
|
329
|
+
const clearPromises = servicesToClearThisRound.map(
|
|
330
|
+
async (serviceName) => {
|
|
331
|
+
try {
|
|
332
|
+
await this.invalidate(serviceName, round)
|
|
333
|
+
clearedServices.add(serviceName)
|
|
334
|
+
this.logger?.log(
|
|
335
|
+
`[ServiceInvalidator] Successfully cleared service: ${serviceName}`,
|
|
336
|
+
)
|
|
337
|
+
} catch (error) {
|
|
338
|
+
this.logger?.error(
|
|
339
|
+
`[ServiceInvalidator] Error clearing service ${serviceName}:`,
|
|
340
|
+
error,
|
|
341
|
+
)
|
|
342
|
+
// Still mark as cleared to avoid infinite loops
|
|
343
|
+
clearedServices.add(serviceName)
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
await Promise.all(clearPromises)
|
|
349
|
+
round++
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (clearedServices.size < serviceNames.length) {
|
|
353
|
+
this.logger?.warn(
|
|
354
|
+
`[ServiceInvalidator] Clearing completed after ${maxRounds} rounds, but ${serviceNames.length - clearedServices.size} services may not have been properly cleared`,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Finds services that are ready to be cleared in the current round.
|
|
361
|
+
* A service is ready if all its dependencies have already been cleared.
|
|
362
|
+
*/
|
|
363
|
+
private findServicesReadyForClearing(
|
|
364
|
+
allServiceNames: string[],
|
|
365
|
+
clearedServices: Set<string>,
|
|
366
|
+
): string[] {
|
|
367
|
+
return allServiceNames.filter((serviceName) => {
|
|
368
|
+
if (clearedServices.has(serviceName)) {
|
|
369
|
+
return false // Already cleared
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check if this service has any dependencies that haven't been cleared yet
|
|
373
|
+
const [error, holder] = this.manager.get(serviceName)
|
|
374
|
+
if (error) {
|
|
375
|
+
return true // Service not found or in error state, can be cleared
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check if all dependencies have been cleared
|
|
379
|
+
const hasUnclearedDependencies = Array.from(holder.deps).some(
|
|
380
|
+
(dep) => !clearedServices.has(dep),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return !hasUnclearedDependencies
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Force clears services that couldn't be cleared through normal dependency resolution.
|
|
389
|
+
* This handles edge cases like circular dependencies.
|
|
390
|
+
*/
|
|
391
|
+
private async forceClearServices(serviceNames: string[]): Promise<void> {
|
|
392
|
+
const promises = serviceNames.map(async (serviceName) => {
|
|
393
|
+
try {
|
|
394
|
+
// Directly destroy the holder without going through normal invalidation
|
|
395
|
+
const [error, holder] = this.manager.get(serviceName)
|
|
396
|
+
if (!error && holder) {
|
|
397
|
+
await this.destroyHolder(serviceName, holder)
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
this.logger?.error(
|
|
401
|
+
`[ServiceInvalidator] Error force clearing service ${serviceName}:`,
|
|
402
|
+
error,
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
await Promise.all(promises)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Gets all service names currently managed by the ServiceLocator.
|
|
412
|
+
*/
|
|
413
|
+
private getAllServiceNames(): string[] {
|
|
414
|
+
return this.manager.getAllNames()
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Emits events to listeners for instance lifecycle events.
|
|
419
|
+
*/
|
|
420
|
+
private emitInstanceEvent(
|
|
421
|
+
name: string,
|
|
422
|
+
event: 'create' | 'destroy' = 'create',
|
|
423
|
+
) {
|
|
424
|
+
this.logger?.log(
|
|
425
|
+
`[ServiceInvalidator]#emitInstanceEvent() Notifying listeners for ${name} with event ${event}`,
|
|
426
|
+
)
|
|
427
|
+
return this.eventBus.emit(name, event)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
@@ -22,7 +22,6 @@ export interface ServiceLocatorInstanceHolderCreating<Instance> {
|
|
|
22
22
|
deps: Set<string>
|
|
23
23
|
destroyListeners: ServiceLocatorInstanceDestroyListener[]
|
|
24
24
|
createdAt: number
|
|
25
|
-
ttl: number
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
export interface ServiceLocatorInstanceHolderCreated<Instance> {
|
|
@@ -36,7 +35,6 @@ export interface ServiceLocatorInstanceHolderCreated<Instance> {
|
|
|
36
35
|
deps: Set<string>
|
|
37
36
|
destroyListeners: ServiceLocatorInstanceDestroyListener[]
|
|
38
37
|
createdAt: number
|
|
39
|
-
ttl: number
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
export interface ServiceLocatorInstanceHolderDestroying<Instance> {
|
|
@@ -50,7 +48,6 @@ export interface ServiceLocatorInstanceHolderDestroying<Instance> {
|
|
|
50
48
|
deps: Set<string>
|
|
51
49
|
destroyListeners: ServiceLocatorInstanceDestroyListener[]
|
|
52
50
|
createdAt: number
|
|
53
|
-
ttl: number
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
export interface ServiceLocatorInstanceHolderError {
|
|
@@ -64,7 +61,6 @@ export interface ServiceLocatorInstanceHolderError {
|
|
|
64
61
|
deps: Set<string>
|
|
65
62
|
destroyListeners: ServiceLocatorInstanceDestroyListener[]
|
|
66
63
|
createdAt: number
|
|
67
|
-
ttl: number
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
export type ServiceLocatorInstanceHolder<Instance = unknown> =
|