@navios/di 0.2.0 → 0.3.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 +301 -39
- package/docs/README.md +122 -49
- package/docs/api-reference.md +763 -0
- package/docs/container.md +274 -0
- package/docs/examples/basic-usage.mts +97 -0
- package/docs/examples/factory-pattern.mts +318 -0
- package/docs/examples/injection-tokens.mts +225 -0
- package/docs/examples/request-scope-example.mts +254 -0
- package/docs/examples/service-lifecycle.mts +359 -0
- package/docs/factory.md +584 -0
- package/docs/getting-started.md +308 -0
- package/docs/injectable.md +496 -0
- package/docs/injection-tokens.md +400 -0
- package/docs/lifecycle.md +539 -0
- package/docs/scopes.md +749 -0
- package/lib/_tsup-dts-rollup.d.mts +495 -150
- package/lib/_tsup-dts-rollup.d.ts +495 -150
- package/lib/index.d.mts +26 -12
- package/lib/index.d.ts +26 -12
- package/lib/index.js +993 -462
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +983 -453
- package/lib/index.mjs.map +1 -1
- package/package.json +2 -2
- package/project.json +10 -2
- package/src/__tests__/container.spec.mts +1301 -0
- package/src/__tests__/factory.spec.mts +137 -0
- package/src/__tests__/injectable.spec.mts +32 -88
- package/src/__tests__/injection-token.spec.mts +333 -17
- package/src/__tests__/request-scope.spec.mts +263 -0
- package/src/__type-tests__/factory.spec-d.mts +65 -0
- package/src/__type-tests__/inject.spec-d.mts +27 -28
- package/src/__type-tests__/injectable.spec-d.mts +42 -206
- package/src/container.mts +167 -0
- package/src/decorators/factory.decorator.mts +79 -0
- package/src/decorators/index.mts +1 -0
- package/src/decorators/injectable.decorator.mts +6 -56
- package/src/enums/injectable-scope.enum.mts +5 -1
- package/src/event-emitter.mts +18 -20
- package/src/factory-context.mts +2 -10
- package/src/index.mts +3 -2
- package/src/injection-token.mts +24 -9
- package/src/injector.mts +8 -20
- package/src/interfaces/factory.interface.mts +3 -3
- package/src/interfaces/index.mts +2 -0
- package/src/interfaces/on-service-destroy.interface.mts +3 -0
- package/src/interfaces/on-service-init.interface.mts +3 -0
- package/src/registry.mts +7 -16
- package/src/request-context-holder.mts +145 -0
- package/src/service-instantiator.mts +158 -0
- package/src/service-locator-event-bus.mts +0 -28
- package/src/service-locator-instance-holder.mts +27 -16
- package/src/service-locator-manager.mts +84 -0
- package/src/service-locator.mts +550 -395
- package/src/utils/defer.mts +73 -0
- package/src/utils/get-injectors.mts +93 -80
- package/src/utils/index.mts +2 -0
- package/src/utils/types.mts +52 -0
- package/docs/concepts/injectable.md +0 -182
- package/docs/concepts/injection-token.md +0 -145
- package/src/proxy-service-locator.mts +0 -83
- package/src/resolve-service.mts +0 -41
package/src/service-locator.mts
CHANGED
|
@@ -4,12 +4,16 @@ import type { z, ZodObject, ZodOptional } from 'zod/v4'
|
|
|
4
4
|
|
|
5
5
|
import type { FactoryContext } from './factory-context.mjs'
|
|
6
6
|
import type {
|
|
7
|
+
AnyInjectableType,
|
|
7
8
|
BaseInjectionTokenSchemaType,
|
|
8
9
|
InjectionTokenSchemaType,
|
|
10
|
+
InjectionTokenType,
|
|
9
11
|
OptionalInjectionTokenSchemaType,
|
|
10
12
|
} from './injection-token.mjs'
|
|
11
13
|
import type { Registry } from './registry.mjs'
|
|
14
|
+
import type { RequestContextHolder } from './request-context-holder.mjs'
|
|
12
15
|
import type { ServiceLocatorInstanceHolder } from './service-locator-instance-holder.mjs'
|
|
16
|
+
import type { Injectors } from './utils/index.mjs'
|
|
13
17
|
|
|
14
18
|
import { InjectableScope } from './enums/index.mjs'
|
|
15
19
|
import {
|
|
@@ -23,363 +27,575 @@ import {
|
|
|
23
27
|
FactoryInjectionToken,
|
|
24
28
|
InjectionToken,
|
|
25
29
|
} from './injection-token.mjs'
|
|
30
|
+
import { defaultInjectors } from './injector.mjs'
|
|
26
31
|
import { globalRegistry } from './registry.mjs'
|
|
32
|
+
import { DefaultRequestContextHolder } from './request-context-holder.mjs'
|
|
33
|
+
import { ServiceInstantiator } from './service-instantiator.mjs'
|
|
27
34
|
import { ServiceLocatorEventBus } from './service-locator-event-bus.mjs'
|
|
28
|
-
import {
|
|
29
|
-
ServiceLocatorInstanceHolderKind,
|
|
30
|
-
ServiceLocatorInstanceHolderStatus,
|
|
31
|
-
} from './service-locator-instance-holder.mjs'
|
|
35
|
+
import { ServiceLocatorInstanceHolderStatus } from './service-locator-instance-holder.mjs'
|
|
32
36
|
import { ServiceLocatorManager } from './service-locator-manager.mjs'
|
|
33
37
|
import { getInjectableToken } from './utils/index.mjs'
|
|
34
38
|
|
|
35
39
|
export class ServiceLocator {
|
|
36
40
|
private readonly eventBus: ServiceLocatorEventBus
|
|
37
41
|
private readonly manager: ServiceLocatorManager
|
|
42
|
+
private readonly serviceInstantiator: ServiceInstantiator
|
|
43
|
+
private readonly requestContexts = new Map<string, RequestContextHolder>()
|
|
44
|
+
private currentRequestContext: RequestContextHolder | null = null
|
|
38
45
|
|
|
39
46
|
constructor(
|
|
40
47
|
private readonly registry: Registry = globalRegistry,
|
|
41
48
|
private readonly logger: Console | null = null,
|
|
49
|
+
private readonly injectors: Injectors = defaultInjectors,
|
|
42
50
|
) {
|
|
43
51
|
this.eventBus = new ServiceLocatorEventBus(logger)
|
|
44
52
|
this.manager = new ServiceLocatorManager(logger)
|
|
53
|
+
this.serviceInstantiator = new ServiceInstantiator(injectors)
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// PUBLIC METHODS
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
47
60
|
getEventBus() {
|
|
48
61
|
return this.eventBus
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
token: BoundInjectionToken<Instance, any>,
|
|
54
|
-
): void
|
|
55
|
-
public storeInstance<Instance>(
|
|
56
|
-
instance: Instance,
|
|
57
|
-
token: FactoryInjectionToken<Instance, any>,
|
|
58
|
-
): void
|
|
59
|
-
public storeInstance<Instance>(
|
|
60
|
-
instance: Instance,
|
|
61
|
-
token: InjectionToken<Instance, undefined>,
|
|
62
|
-
): void
|
|
63
|
-
public storeInstance<
|
|
64
|
-
Instance,
|
|
65
|
-
Schema extends ZodObject<any> | ZodOptional<ZodObject<any>>,
|
|
66
|
-
>(
|
|
67
|
-
instance: Instance,
|
|
68
|
-
token: InjectionToken<Instance, Schema>,
|
|
69
|
-
args: z.input<Schema>,
|
|
70
|
-
): void
|
|
71
|
-
public storeInstance(
|
|
72
|
-
instance: any,
|
|
73
|
-
token:
|
|
74
|
-
| InjectionToken<any, any>
|
|
75
|
-
| BoundInjectionToken<any, any>
|
|
76
|
-
| FactoryInjectionToken<any, any>,
|
|
77
|
-
args?: any,
|
|
78
|
-
): void {
|
|
79
|
-
// @ts-expect-error We should redefine the instance name type
|
|
80
|
-
const instanceName = this.getInstanceIdentifier(token, args)
|
|
81
|
-
this.manager.set(instanceName, {
|
|
82
|
-
name: instanceName,
|
|
83
|
-
instance,
|
|
84
|
-
status: ServiceLocatorInstanceHolderStatus.Created,
|
|
85
|
-
kind: ServiceLocatorInstanceHolderKind.Instance,
|
|
86
|
-
createdAt: Date.now(),
|
|
87
|
-
ttl: Infinity,
|
|
88
|
-
deps: [],
|
|
89
|
-
destroyListeners: [],
|
|
90
|
-
effects: [],
|
|
91
|
-
destroyPromise: null,
|
|
92
|
-
creationPromise: null,
|
|
93
|
-
})
|
|
94
|
-
this.notifyListeners(instanceName)
|
|
64
|
+
getManager() {
|
|
65
|
+
return this.manager
|
|
95
66
|
}
|
|
96
67
|
|
|
97
|
-
public
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
public
|
|
107
|
-
token:
|
|
108
|
-
args: z.input<Schema>,
|
|
109
|
-
): void
|
|
110
|
-
public removeInstance<
|
|
111
|
-
Instance,
|
|
112
|
-
Schema extends OptionalInjectionTokenSchemaType,
|
|
113
|
-
>(token: InjectionToken<Instance, Schema>, args?: z.input<Schema>): void
|
|
114
|
-
|
|
115
|
-
public removeInstance(
|
|
116
|
-
token:
|
|
117
|
-
| InjectionToken<any, any>
|
|
118
|
-
| BoundInjectionToken<any, any>
|
|
119
|
-
| FactoryInjectionToken<any, any>,
|
|
68
|
+
public getInstanceIdentifier(token: AnyInjectableType, args?: any): string {
|
|
69
|
+
const [err, { actualToken, validatedArgs }] =
|
|
70
|
+
this.validateAndResolveTokenArgs(token, args)
|
|
71
|
+
if (err) {
|
|
72
|
+
throw err
|
|
73
|
+
}
|
|
74
|
+
return this.generateInstanceName(actualToken, validatedArgs)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async getInstance(
|
|
78
|
+
token: AnyInjectableType,
|
|
120
79
|
args?: any,
|
|
80
|
+
onPrepare?: (data: {
|
|
81
|
+
instanceName: string
|
|
82
|
+
actualToken: InjectionTokenType
|
|
83
|
+
validatedArgs?: any
|
|
84
|
+
}) => void,
|
|
121
85
|
) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
86
|
+
const [err, data] = await this.resolveTokenAndPrepareInstanceName(
|
|
87
|
+
token,
|
|
88
|
+
args,
|
|
89
|
+
)
|
|
90
|
+
if (err) {
|
|
91
|
+
return [err]
|
|
92
|
+
}
|
|
93
|
+
const { instanceName, validatedArgs, actualToken, realToken } = data
|
|
94
|
+
if (onPrepare) {
|
|
95
|
+
onPrepare({ instanceName, actualToken, validatedArgs })
|
|
96
|
+
}
|
|
97
|
+
const [error, holder] = await this.retrieveOrCreateInstanceByInstanceName(
|
|
98
|
+
instanceName,
|
|
99
|
+
realToken,
|
|
100
|
+
validatedArgs,
|
|
101
|
+
)
|
|
102
|
+
if (error) {
|
|
103
|
+
return [error]
|
|
104
|
+
}
|
|
105
|
+
return [undefined, holder.instance]
|
|
125
106
|
}
|
|
126
107
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
108
|
+
public async getOrThrowInstance<Instance>(
|
|
109
|
+
token: AnyInjectableType,
|
|
110
|
+
args: any,
|
|
111
|
+
): Promise<Instance> {
|
|
112
|
+
const [error, instance] = await this.getInstance(token, args)
|
|
113
|
+
if (error) {
|
|
114
|
+
throw error
|
|
115
|
+
}
|
|
116
|
+
return instance
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getSyncInstance<
|
|
135
120
|
Instance,
|
|
136
|
-
Schema extends
|
|
121
|
+
Schema extends InjectionTokenSchemaType | undefined,
|
|
137
122
|
>(
|
|
138
|
-
token:
|
|
139
|
-
args
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
123
|
+
token: AnyInjectableType,
|
|
124
|
+
args: Schema extends ZodObject
|
|
125
|
+
? z.input<Schema>
|
|
126
|
+
: Schema extends ZodOptional<ZodObject>
|
|
127
|
+
? z.input<Schema> | undefined
|
|
128
|
+
: undefined,
|
|
129
|
+
): Instance | null {
|
|
130
|
+
const [err, { actualToken, validatedArgs }] =
|
|
131
|
+
this.validateAndResolveTokenArgs(token, args)
|
|
132
|
+
if (err) {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
const instanceName = this.generateInstanceName(actualToken, validatedArgs)
|
|
136
|
+
if (this.currentRequestContext) {
|
|
137
|
+
const requestHolder = this.currentRequestContext.getHolder(instanceName)
|
|
138
|
+
if (requestHolder) {
|
|
139
|
+
return requestHolder.instance as Instance
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const [error, holder] = this.manager.get(instanceName)
|
|
143
|
+
if (error) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
return holder.instance as Instance
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
invalidate(service: string, round = 1): Promise<any> {
|
|
150
|
+
this.logger?.log(
|
|
151
|
+
`[ServiceLocator]#invalidate(): Starting Invalidating process of ${service}`,
|
|
152
|
+
)
|
|
153
|
+
const toInvalidate = this.manager.filter(
|
|
154
|
+
(holder) => holder.name === service || holder.deps.has(service),
|
|
155
|
+
)
|
|
156
|
+
const promises = []
|
|
157
|
+
for (const [key, holder] of toInvalidate.entries()) {
|
|
158
|
+
if (holder.status === ServiceLocatorInstanceHolderStatus.Destroying) {
|
|
159
|
+
this.logger?.trace(
|
|
160
|
+
`[ServiceLocator]#invalidate(): ${key} is already being destroyed`,
|
|
161
|
+
)
|
|
162
|
+
promises.push(holder.destroyPromise)
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
|
|
166
|
+
this.logger?.trace(
|
|
167
|
+
`[ServiceLocator]#invalidate(): ${key} is being created, waiting for creation to finish`,
|
|
168
|
+
)
|
|
169
|
+
promises.push(
|
|
170
|
+
holder.creationPromise?.then(() => {
|
|
171
|
+
if (round > 3) {
|
|
172
|
+
this.logger?.error(
|
|
173
|
+
`[ServiceLocator]#invalidate(): ${key} creation is triggering a new invalidation round, but it is still not created`,
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
return this.invalidate(key, round + 1)
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
// @ts-expect-error TS2322 we are changing the status
|
|
183
|
+
holder.status = ServiceLocatorInstanceHolderStatus.Destroying
|
|
184
|
+
this.logger?.log(
|
|
185
|
+
`[ServiceLocator]#invalidate(): Invalidating ${key} and notifying listeners`,
|
|
186
|
+
)
|
|
187
|
+
// @ts-expect-error TS2322 we are changing the status
|
|
188
|
+
holder.destroyPromise = Promise.all(
|
|
189
|
+
holder.destroyListeners.map((listener) => listener()),
|
|
190
|
+
).then(async () => {
|
|
191
|
+
this.manager.delete(key)
|
|
192
|
+
await this.emitInstanceEvent(key, 'destroy')
|
|
193
|
+
})
|
|
194
|
+
promises.push(holder.destroyPromise)
|
|
195
|
+
}
|
|
196
|
+
return Promise.all(promises)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async ready() {
|
|
200
|
+
return Promise.all(
|
|
201
|
+
Array.from(this.manager.filter(() => true)).map(([, holder]) => {
|
|
202
|
+
if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
|
|
203
|
+
return holder.creationPromise?.then(() => null)
|
|
204
|
+
}
|
|
205
|
+
if (holder.status === ServiceLocatorInstanceHolderStatus.Destroying) {
|
|
206
|
+
return holder.destroyPromise.then(() => null)
|
|
207
|
+
}
|
|
208
|
+
return Promise.resolve(null)
|
|
209
|
+
}),
|
|
210
|
+
).then(() => null)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// REQUEST CONTEXT MANAGEMENT
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Begins a new request context with the given parameters.
|
|
219
|
+
* @param requestId Unique identifier for this request
|
|
220
|
+
* @param metadata Optional metadata for the request
|
|
221
|
+
* @param priority Priority for resolution (higher = more priority)
|
|
222
|
+
* @returns The created request context holder
|
|
223
|
+
*/
|
|
224
|
+
beginRequest(
|
|
225
|
+
requestId: string,
|
|
226
|
+
metadata?: Record<string, any>,
|
|
227
|
+
priority: number = 100,
|
|
228
|
+
): RequestContextHolder {
|
|
229
|
+
if (this.requestContexts.has(requestId)) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`[ServiceLocator] Request context ${requestId} already exists`,
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const contextHolder = new DefaultRequestContextHolder(
|
|
236
|
+
requestId,
|
|
237
|
+
priority,
|
|
238
|
+
metadata,
|
|
239
|
+
)
|
|
240
|
+
this.requestContexts.set(requestId, contextHolder)
|
|
241
|
+
this.currentRequestContext = contextHolder
|
|
242
|
+
|
|
243
|
+
this.logger?.log(`[ServiceLocator] Started request context: ${requestId}`)
|
|
244
|
+
return contextHolder
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Ends a request context and cleans up all associated instances.
|
|
249
|
+
* @param requestId The request ID to end
|
|
250
|
+
*/
|
|
251
|
+
async endRequest(requestId: string): Promise<void> {
|
|
252
|
+
const contextHolder = this.requestContexts.get(requestId)
|
|
253
|
+
if (!contextHolder) {
|
|
254
|
+
this.logger?.warn(
|
|
255
|
+
`[ServiceLocator] Request context ${requestId} not found`,
|
|
256
|
+
)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.logger?.log(`[ServiceLocator] Ending request context: ${requestId}`)
|
|
261
|
+
|
|
262
|
+
// Clean up all request-scoped instances
|
|
263
|
+
const cleanupPromises: Promise<any>[] = []
|
|
264
|
+
for (const [, holder] of contextHolder.holders) {
|
|
265
|
+
if (holder.destroyListeners.length > 0) {
|
|
266
|
+
cleanupPromises.push(
|
|
267
|
+
Promise.all(holder.destroyListeners.map((listener) => listener())),
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await Promise.all(cleanupPromises)
|
|
273
|
+
|
|
274
|
+
// Clear the context
|
|
275
|
+
contextHolder.clear()
|
|
276
|
+
this.requestContexts.delete(requestId)
|
|
277
|
+
|
|
278
|
+
// Reset current context if it was the one being ended
|
|
279
|
+
if (this.currentRequestContext === contextHolder) {
|
|
280
|
+
this.currentRequestContext =
|
|
281
|
+
Array.from(this.requestContexts.values()).at(-1) ?? null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.logger?.log(`[ServiceLocator] Request context ${requestId} ended`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Gets the current request context.
|
|
289
|
+
* @returns The current request context holder or null
|
|
290
|
+
*/
|
|
291
|
+
getCurrentRequestContext(): RequestContextHolder | null {
|
|
292
|
+
return this.currentRequestContext
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Sets the current request context.
|
|
297
|
+
* @param requestId The request ID to set as current
|
|
298
|
+
*/
|
|
299
|
+
setCurrentRequestContext(requestId: string): void {
|
|
300
|
+
const contextHolder = this.requestContexts.get(requestId)
|
|
301
|
+
if (!contextHolder) {
|
|
302
|
+
throw new Error(`[ServiceLocator] Request context ${requestId} not found`)
|
|
303
|
+
}
|
|
304
|
+
this.currentRequestContext = contextHolder
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ============================================================================
|
|
308
|
+
// PRIVATE METHODS
|
|
309
|
+
// ============================================================================
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Validates and resolves token arguments, handling factory token resolution and validation.
|
|
313
|
+
*/
|
|
314
|
+
private validateAndResolveTokenArgs(
|
|
315
|
+
token: AnyInjectableType,
|
|
152
316
|
args?: any,
|
|
153
|
-
)
|
|
317
|
+
): [
|
|
318
|
+
FactoryTokenNotResolved | UnknownError | undefined,
|
|
319
|
+
{ actualToken: InjectionTokenType; validatedArgs?: any },
|
|
320
|
+
] {
|
|
321
|
+
let actualToken = token as InjectionToken<any, any>
|
|
322
|
+
if (typeof token === 'function') {
|
|
323
|
+
actualToken = getInjectableToken(token)
|
|
324
|
+
}
|
|
154
325
|
let realArgs = args
|
|
155
|
-
if (
|
|
156
|
-
realArgs =
|
|
157
|
-
} else if (
|
|
158
|
-
if (
|
|
159
|
-
realArgs =
|
|
326
|
+
if (actualToken instanceof BoundInjectionToken) {
|
|
327
|
+
realArgs = actualToken.value
|
|
328
|
+
} else if (actualToken instanceof FactoryInjectionToken) {
|
|
329
|
+
if (actualToken.resolved) {
|
|
330
|
+
realArgs = actualToken.value
|
|
160
331
|
} else {
|
|
161
|
-
return [new FactoryTokenNotResolved(token.name)]
|
|
332
|
+
return [new FactoryTokenNotResolved(token.name), { actualToken }]
|
|
162
333
|
}
|
|
163
334
|
}
|
|
164
|
-
if (!
|
|
165
|
-
return [undefined, realArgs]
|
|
335
|
+
if (!actualToken.schema) {
|
|
336
|
+
return [undefined, { actualToken, validatedArgs: realArgs }]
|
|
166
337
|
}
|
|
167
|
-
const validatedArgs =
|
|
338
|
+
const validatedArgs = actualToken.schema?.safeParse(realArgs)
|
|
168
339
|
if (validatedArgs && !validatedArgs.success) {
|
|
169
340
|
this.logger?.error(
|
|
170
|
-
`[ServiceLocator]#
|
|
341
|
+
`[ServiceLocator]#validateAndResolveTokenArgs(): Error validating args for ${actualToken.name.toString()}`,
|
|
171
342
|
validatedArgs.error,
|
|
172
343
|
)
|
|
173
|
-
return [new UnknownError(validatedArgs.error)]
|
|
344
|
+
return [new UnknownError(validatedArgs.error), { actualToken }]
|
|
174
345
|
}
|
|
175
|
-
return [undefined, validatedArgs?.data]
|
|
346
|
+
return [undefined, { actualToken, validatedArgs: validatedArgs?.data }]
|
|
176
347
|
}
|
|
177
348
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Schema extends OptionalInjectionTokenSchemaType,
|
|
185
|
-
>(token: InjectionToken<Instance, Schema>, args?: z.input<Schema>): string
|
|
186
|
-
public getInstanceIdentifier<Instance>(
|
|
187
|
-
token: InjectionToken<Instance, undefined>,
|
|
188
|
-
): string
|
|
189
|
-
public getInstanceIdentifier<Instance>(
|
|
190
|
-
token: BoundInjectionToken<Instance, any>,
|
|
191
|
-
): string
|
|
192
|
-
public getInstanceIdentifier<Instance>(
|
|
193
|
-
token: FactoryInjectionToken<Instance, any>,
|
|
194
|
-
): string
|
|
195
|
-
public getInstanceIdentifier(
|
|
196
|
-
token:
|
|
197
|
-
| InjectionToken<any, any>
|
|
198
|
-
| BoundInjectionToken<any, any>
|
|
199
|
-
| FactoryInjectionToken<any, any>,
|
|
200
|
-
args?: any,
|
|
201
|
-
): string {
|
|
202
|
-
const [err, realArgs] = this.resolveTokenArgs(
|
|
203
|
-
token as InjectionToken<any>,
|
|
204
|
-
args,
|
|
205
|
-
)
|
|
206
|
-
if (err) {
|
|
207
|
-
throw err
|
|
208
|
-
}
|
|
209
|
-
return this.makeInstanceName(token as InjectionToken<any>, realArgs)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
public getInstance<Instance, Schema extends BaseInjectionTokenSchemaType>(
|
|
213
|
-
token: InjectionToken<Instance, Schema>,
|
|
214
|
-
args: z.input<Schema>,
|
|
215
|
-
): Promise<[undefined, Instance] | [UnknownError | FactoryNotFound]>
|
|
216
|
-
public getInstance<Instance, Schema extends OptionalInjectionTokenSchemaType>(
|
|
217
|
-
token: InjectionToken<Instance, Schema>,
|
|
218
|
-
args?: z.input<Schema>,
|
|
219
|
-
): Promise<[undefined, Instance] | [UnknownError | FactoryNotFound]>
|
|
220
|
-
public getInstance<Instance>(
|
|
221
|
-
token: InjectionToken<Instance, undefined>,
|
|
222
|
-
): Promise<[undefined, Instance] | [UnknownError | FactoryNotFound]>
|
|
223
|
-
public getInstance<Instance>(
|
|
224
|
-
token: BoundInjectionToken<Instance, any>,
|
|
225
|
-
): Promise<[undefined, Instance] | [UnknownError | FactoryNotFound]>
|
|
226
|
-
public getInstance<Instance>(
|
|
227
|
-
token: FactoryInjectionToken<Instance, any>,
|
|
228
|
-
): Promise<[undefined, Instance] | [UnknownError | FactoryNotFound]>
|
|
229
|
-
|
|
230
|
-
public async getInstance(
|
|
231
|
-
token:
|
|
232
|
-
| InjectionToken<any, any>
|
|
233
|
-
| BoundInjectionToken<any, any>
|
|
234
|
-
| FactoryInjectionToken<any, any>,
|
|
349
|
+
/**
|
|
350
|
+
* Internal method to resolve token args and create instance name.
|
|
351
|
+
* Handles factory token resolution and validation.
|
|
352
|
+
*/
|
|
353
|
+
private async resolveTokenAndPrepareInstanceName(
|
|
354
|
+
token: AnyInjectableType,
|
|
235
355
|
args?: any,
|
|
236
|
-
)
|
|
237
|
-
|
|
356
|
+
): Promise<
|
|
357
|
+
| [
|
|
358
|
+
undefined,
|
|
359
|
+
{
|
|
360
|
+
instanceName: string
|
|
361
|
+
validatedArgs: any
|
|
362
|
+
actualToken: InjectionTokenType
|
|
363
|
+
realToken: InjectionToken<any, any>
|
|
364
|
+
},
|
|
365
|
+
]
|
|
366
|
+
| [UnknownError | FactoryTokenNotResolved]
|
|
367
|
+
> {
|
|
368
|
+
const [err, { actualToken, validatedArgs }] =
|
|
369
|
+
this.validateAndResolveTokenArgs(token, args)
|
|
238
370
|
if (err instanceof UnknownError) {
|
|
239
371
|
return [err]
|
|
240
372
|
} else if (
|
|
241
373
|
(err as any) instanceof FactoryTokenNotResolved &&
|
|
242
|
-
|
|
374
|
+
actualToken instanceof FactoryInjectionToken
|
|
243
375
|
) {
|
|
244
376
|
this.logger?.log(
|
|
245
|
-
`[ServiceLocator]#
|
|
377
|
+
`[ServiceLocator]#resolveTokenAndPrepareInstanceName() Factory token not resolved, resolving it`,
|
|
246
378
|
)
|
|
247
|
-
await
|
|
248
|
-
return this.
|
|
379
|
+
await actualToken.resolve(this.createFactoryContext())
|
|
380
|
+
return this.resolveTokenAndPrepareInstanceName(token)
|
|
381
|
+
}
|
|
382
|
+
const instanceName = this.generateInstanceName(actualToken, validatedArgs)
|
|
383
|
+
// Determine the real token (the actual InjectionToken that will be used for resolution)
|
|
384
|
+
const realToken =
|
|
385
|
+
actualToken instanceof BoundInjectionToken ||
|
|
386
|
+
actualToken instanceof FactoryInjectionToken
|
|
387
|
+
? actualToken.token
|
|
388
|
+
: actualToken
|
|
389
|
+
return [undefined, { instanceName, validatedArgs, actualToken, realToken }]
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Gets an instance by its instance name, handling all the logic after instance name creation.
|
|
394
|
+
*/
|
|
395
|
+
private async retrieveOrCreateInstanceByInstanceName(
|
|
396
|
+
instanceName: string,
|
|
397
|
+
realToken: InjectionToken<any, any>,
|
|
398
|
+
realArgs: any,
|
|
399
|
+
): Promise<
|
|
400
|
+
| [undefined, ServiceLocatorInstanceHolder<any>]
|
|
401
|
+
| [UnknownError | FactoryNotFound]
|
|
402
|
+
> {
|
|
403
|
+
// Check if this is a request-scoped service and we have a current request context
|
|
404
|
+
if (this.registry.has(realToken)) {
|
|
405
|
+
const record = this.registry.get(realToken)
|
|
406
|
+
if (record.scope === InjectableScope.Request) {
|
|
407
|
+
if (!this.currentRequestContext) {
|
|
408
|
+
this.logger?.log(
|
|
409
|
+
`[ServiceLocator]#retrieveOrCreateInstanceByInstanceName() No current request context available for request-scoped service ${instanceName}`,
|
|
410
|
+
)
|
|
411
|
+
return [new UnknownError(ErrorsEnum.InstanceNotFound)]
|
|
412
|
+
}
|
|
413
|
+
const requestHolder = this.currentRequestContext.getHolder(instanceName)
|
|
414
|
+
if (requestHolder) {
|
|
415
|
+
if (
|
|
416
|
+
requestHolder.status === ServiceLocatorInstanceHolderStatus.Creating
|
|
417
|
+
) {
|
|
418
|
+
await requestHolder.creationPromise
|
|
419
|
+
return this.retrieveOrCreateInstanceByInstanceName(
|
|
420
|
+
instanceName,
|
|
421
|
+
realToken,
|
|
422
|
+
realArgs,
|
|
423
|
+
)
|
|
424
|
+
} else if (
|
|
425
|
+
requestHolder.status ===
|
|
426
|
+
ServiceLocatorInstanceHolderStatus.Destroying
|
|
427
|
+
) {
|
|
428
|
+
return [new UnknownError(ErrorsEnum.InstanceDestroying)]
|
|
429
|
+
}
|
|
430
|
+
return [undefined, requestHolder]
|
|
431
|
+
}
|
|
432
|
+
}
|
|
249
433
|
}
|
|
250
|
-
|
|
434
|
+
|
|
251
435
|
const [error, holder] = this.manager.get(instanceName)
|
|
252
436
|
if (!error) {
|
|
253
437
|
if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
|
|
254
|
-
|
|
438
|
+
await holder.creationPromise
|
|
439
|
+
return this.retrieveOrCreateInstanceByInstanceName(
|
|
440
|
+
instanceName,
|
|
441
|
+
realToken,
|
|
442
|
+
realArgs,
|
|
443
|
+
)
|
|
255
444
|
} else if (
|
|
256
445
|
holder.status === ServiceLocatorInstanceHolderStatus.Destroying
|
|
257
446
|
) {
|
|
258
447
|
// Should never happen
|
|
259
448
|
return [new UnknownError(ErrorsEnum.InstanceDestroying)]
|
|
260
449
|
}
|
|
261
|
-
return [undefined, holder
|
|
450
|
+
return [undefined, holder]
|
|
262
451
|
}
|
|
263
452
|
switch (error.code) {
|
|
264
453
|
case ErrorsEnum.InstanceDestroying:
|
|
265
454
|
this.logger?.log(
|
|
266
|
-
`[ServiceLocator]#
|
|
455
|
+
`[ServiceLocator]#retrieveOrCreateInstanceByInstanceName() TTL expired for ${holder?.name}`,
|
|
267
456
|
)
|
|
268
457
|
await holder?.destroyPromise
|
|
269
458
|
//Maybe we already have a new instance
|
|
270
|
-
|
|
271
|
-
|
|
459
|
+
return this.retrieveOrCreateInstanceByInstanceName(
|
|
460
|
+
instanceName,
|
|
461
|
+
realToken,
|
|
462
|
+
realArgs,
|
|
463
|
+
)
|
|
272
464
|
|
|
273
465
|
case ErrorsEnum.InstanceExpired:
|
|
274
466
|
this.logger?.log(
|
|
275
|
-
`[ServiceLocator]#
|
|
467
|
+
`[ServiceLocator]#retrieveOrCreateInstanceByInstanceName() TTL expired for ${holder?.name}`,
|
|
276
468
|
)
|
|
277
469
|
await this.invalidate(instanceName)
|
|
278
470
|
//Maybe we already have a new instance
|
|
279
|
-
|
|
280
|
-
|
|
471
|
+
return this.retrieveOrCreateInstanceByInstanceName(
|
|
472
|
+
instanceName,
|
|
473
|
+
realToken,
|
|
474
|
+
realArgs,
|
|
475
|
+
)
|
|
281
476
|
case ErrorsEnum.InstanceNotFound:
|
|
282
477
|
break
|
|
283
478
|
default:
|
|
284
479
|
return [error]
|
|
285
480
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
>(
|
|
294
|
-
token: InjectionToken<Instance, Schema>,
|
|
295
|
-
args: Schema extends ZodObject<any>
|
|
296
|
-
? z.input<Schema>
|
|
297
|
-
: Schema extends ZodOptional<ZodObject<any>>
|
|
298
|
-
? z.input<Schema> | undefined
|
|
299
|
-
: undefined,
|
|
300
|
-
): Promise<Instance> {
|
|
301
|
-
const [error, instance] = await this.getInstance(token, args)
|
|
302
|
-
if (error) {
|
|
303
|
-
throw error
|
|
481
|
+
const result = await this.createNewInstance(
|
|
482
|
+
instanceName,
|
|
483
|
+
realToken,
|
|
484
|
+
realArgs,
|
|
485
|
+
)
|
|
486
|
+
if (result[0]) {
|
|
487
|
+
return [result[0]]
|
|
304
488
|
}
|
|
305
|
-
|
|
489
|
+
if (result[1].status === ServiceLocatorInstanceHolderStatus.Creating) {
|
|
490
|
+
await result[1].creationPromise
|
|
491
|
+
}
|
|
492
|
+
if (result[1].status === ServiceLocatorInstanceHolderStatus.Error) {
|
|
493
|
+
return [result[1].instance] as [UnknownError | FactoryNotFound]
|
|
494
|
+
}
|
|
495
|
+
return [undefined, result[1]]
|
|
306
496
|
}
|
|
307
497
|
|
|
308
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Emits events to listeners for instance lifecycle events.
|
|
500
|
+
*/
|
|
501
|
+
private emitInstanceEvent(
|
|
309
502
|
name: string,
|
|
310
503
|
event: 'create' | 'destroy' = 'create',
|
|
311
504
|
) {
|
|
312
505
|
this.logger?.log(
|
|
313
|
-
`[ServiceLocator]#
|
|
506
|
+
`[ServiceLocator]#emitInstanceEvent() Notifying listeners for ${name} with event ${event}`,
|
|
314
507
|
)
|
|
315
508
|
return this.eventBus.emit(name, event)
|
|
316
509
|
}
|
|
317
510
|
|
|
318
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Creates a new instance for the given token and arguments.
|
|
513
|
+
*/
|
|
514
|
+
private async createNewInstance<
|
|
319
515
|
Instance,
|
|
320
516
|
Schema extends InjectionTokenSchemaType | undefined,
|
|
321
517
|
>(
|
|
322
518
|
instanceName: string,
|
|
323
|
-
|
|
324
|
-
args: Schema extends ZodObject
|
|
325
|
-
? z.
|
|
326
|
-
: Schema extends ZodOptional<ZodObject
|
|
327
|
-
? z.
|
|
519
|
+
realToken: InjectionToken<Instance, Schema>,
|
|
520
|
+
args: Schema extends ZodObject
|
|
521
|
+
? z.output<Schema>
|
|
522
|
+
: Schema extends ZodOptional<ZodObject>
|
|
523
|
+
? z.output<Schema> | undefined
|
|
328
524
|
: undefined,
|
|
329
|
-
): Promise<
|
|
525
|
+
): Promise<
|
|
526
|
+
| [undefined, ServiceLocatorInstanceHolder<Instance>]
|
|
527
|
+
| [FactoryNotFound | UnknownError]
|
|
528
|
+
> {
|
|
330
529
|
this.logger?.log(
|
|
331
|
-
`[ServiceLocator]#
|
|
530
|
+
`[ServiceLocator]#createNewInstance() Creating instance for ${instanceName}`,
|
|
332
531
|
)
|
|
333
|
-
let realToken =
|
|
334
|
-
token instanceof BoundInjectionToken ||
|
|
335
|
-
token instanceof FactoryInjectionToken
|
|
336
|
-
? token.token
|
|
337
|
-
: token
|
|
338
532
|
if (this.registry.has(realToken)) {
|
|
339
|
-
return this.
|
|
533
|
+
return this.instantiateServiceFromRegistry<Instance, Schema, any>(
|
|
534
|
+
instanceName,
|
|
535
|
+
realToken,
|
|
536
|
+
args,
|
|
537
|
+
)
|
|
340
538
|
} else {
|
|
341
539
|
return [new FactoryNotFound(realToken.name.toString())]
|
|
342
540
|
}
|
|
343
541
|
}
|
|
344
542
|
|
|
345
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Instantiates a service from the registry using the service instantiator.
|
|
545
|
+
*/
|
|
546
|
+
private instantiateServiceFromRegistry<
|
|
346
547
|
Instance,
|
|
347
548
|
Schema extends InjectionTokenSchemaType | undefined,
|
|
348
549
|
Args extends Schema extends BaseInjectionTokenSchemaType
|
|
349
|
-
? z.
|
|
550
|
+
? z.output<Schema>
|
|
350
551
|
: Schema extends OptionalInjectionTokenSchemaType
|
|
351
|
-
? z.
|
|
552
|
+
? z.output<Schema> | undefined
|
|
352
553
|
: undefined,
|
|
353
554
|
>(
|
|
354
555
|
instanceName: string,
|
|
355
556
|
token: InjectionToken<Instance, Schema>,
|
|
356
557
|
args: Args,
|
|
357
|
-
): Promise<[undefined, Instance]
|
|
558
|
+
): Promise<[undefined, ServiceLocatorInstanceHolder<Instance>]> {
|
|
358
559
|
this.logger?.log(
|
|
359
|
-
`[ServiceLocator]#
|
|
560
|
+
`[ServiceLocator]#instantiateServiceFromRegistry(): Creating instance for ${instanceName} from abstract factory`,
|
|
561
|
+
)
|
|
562
|
+
const ctx = this.createFactoryContext(
|
|
563
|
+
this.currentRequestContext || undefined,
|
|
360
564
|
)
|
|
361
|
-
|
|
362
|
-
let {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
565
|
+
let record = this.registry.get<Instance, Schema>(token)
|
|
566
|
+
let { scope, type } = record
|
|
567
|
+
|
|
568
|
+
// Use createCreatingHolder from manager
|
|
569
|
+
const [deferred, holder] = this.manager.createCreatingHolder<Instance>(
|
|
570
|
+
instanceName,
|
|
571
|
+
type,
|
|
572
|
+
scope,
|
|
573
|
+
ctx.deps,
|
|
574
|
+
Infinity,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
// Start the instantiation process
|
|
578
|
+
this.serviceInstantiator
|
|
579
|
+
.instantiateService(ctx, record, args)
|
|
580
|
+
.then(async ([error, instance]) => {
|
|
581
|
+
holder.destroyListeners = ctx.getDestroyListeners()
|
|
582
|
+
holder.creationPromise = null
|
|
583
|
+
if (error) {
|
|
584
|
+
this.logger?.error(
|
|
585
|
+
`[ServiceLocator]#instantiateServiceFromRegistry(): Error creating instance for ${instanceName}`,
|
|
586
|
+
error,
|
|
587
|
+
)
|
|
588
|
+
holder.status = ServiceLocatorInstanceHolderStatus.Error
|
|
589
|
+
holder.instance = error
|
|
590
|
+
if (scope === InjectableScope.Singleton) {
|
|
591
|
+
setTimeout(() => this.invalidate(instanceName), 10)
|
|
592
|
+
}
|
|
593
|
+
deferred.reject(error)
|
|
594
|
+
} else {
|
|
371
595
|
holder.instance = instance
|
|
372
596
|
holder.status = ServiceLocatorInstanceHolderStatus.Created
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
holder.ttl = ctx.getTtl()
|
|
376
|
-
if (holder.deps.length > 0) {
|
|
377
|
-
this.logger?.log(
|
|
378
|
-
`[ServiceLocator]#createInstanceFromAbstractFactory(): Adding subscriptions for ${instanceName} dependencies for their invalidations: ${holder.deps.join(
|
|
379
|
-
', ',
|
|
380
|
-
)}`,
|
|
381
|
-
)
|
|
382
|
-
holder.deps.forEach((dependency) => {
|
|
597
|
+
if (ctx.deps.size > 0) {
|
|
598
|
+
ctx.deps.forEach((dependency) => {
|
|
383
599
|
holder.destroyListeners.push(
|
|
384
600
|
this.eventBus.on(dependency, 'destroy', () =>
|
|
385
601
|
this.invalidate(instanceName),
|
|
@@ -387,189 +603,128 @@ export class ServiceLocator {
|
|
|
387
603
|
)
|
|
388
604
|
})
|
|
389
605
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
createdAt: Date.now(),
|
|
408
|
-
ttl: Infinity,
|
|
409
|
-
}
|
|
606
|
+
await this.emitInstanceEvent(instanceName)
|
|
607
|
+
deferred.resolve([undefined, instance])
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
.catch((error) => {
|
|
611
|
+
this.logger?.error(
|
|
612
|
+
`[ServiceLocator]#instantiateServiceFromRegistry(): Unexpected error creating instance for ${instanceName}`,
|
|
613
|
+
error,
|
|
614
|
+
)
|
|
615
|
+
holder.status = ServiceLocatorInstanceHolderStatus.Error
|
|
616
|
+
holder.instance = error
|
|
617
|
+
holder.creationPromise = null
|
|
618
|
+
if (scope === InjectableScope.Singleton) {
|
|
619
|
+
setTimeout(() => this.invalidate(instanceName), 10)
|
|
620
|
+
}
|
|
621
|
+
deferred.reject(error)
|
|
622
|
+
})
|
|
410
623
|
|
|
411
624
|
if (scope === InjectableScope.Singleton) {
|
|
412
625
|
this.logger?.debug(
|
|
413
|
-
`[ServiceLocator]#
|
|
626
|
+
`[ServiceLocator]#instantiateServiceFromRegistry(): Setting instance for ${instanceName}`,
|
|
414
627
|
)
|
|
415
628
|
this.manager.set(instanceName, holder)
|
|
629
|
+
} else if (
|
|
630
|
+
scope === InjectableScope.Request &&
|
|
631
|
+
this.currentRequestContext
|
|
632
|
+
) {
|
|
633
|
+
this.logger?.debug(
|
|
634
|
+
`[ServiceLocator]#instantiateServiceFromRegistry(): Setting request-scoped instance for ${instanceName}`,
|
|
635
|
+
)
|
|
636
|
+
// For request-scoped services, we don't store them in the global manager
|
|
637
|
+
// They will be managed by the request context holder
|
|
638
|
+
this.currentRequestContext.addInstance(
|
|
639
|
+
instanceName,
|
|
640
|
+
holder.instance,
|
|
641
|
+
holder,
|
|
642
|
+
)
|
|
416
643
|
}
|
|
417
644
|
// @ts-expect-error TS2322 This is correct type
|
|
418
|
-
return holder
|
|
645
|
+
return [undefined, holder]
|
|
419
646
|
}
|
|
420
647
|
|
|
421
|
-
|
|
422
|
-
|
|
648
|
+
/**
|
|
649
|
+
* Creates a factory context for dependency injection during service instantiation.
|
|
650
|
+
* @param contextHolder Optional request context holder for priority-based resolution
|
|
651
|
+
*/
|
|
652
|
+
private createFactoryContext(
|
|
653
|
+
contextHolder?: RequestContextHolder,
|
|
654
|
+
): FactoryContext & {
|
|
655
|
+
getDestroyListeners: () => (() => void)[]
|
|
656
|
+
deps: Set<string>
|
|
657
|
+
} {
|
|
423
658
|
const destroyListeners = new Set<() => void>()
|
|
659
|
+
const deps = new Set<string>()
|
|
424
660
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
425
661
|
const self = this
|
|
426
662
|
|
|
427
|
-
function
|
|
428
|
-
return self.invalidate(name)
|
|
429
|
-
}
|
|
430
|
-
function addEffect(listener: () => void) {
|
|
663
|
+
function addDestroyListener(listener: () => void) {
|
|
431
664
|
destroyListeners.add(listener)
|
|
432
665
|
}
|
|
433
|
-
let ttl = Infinity
|
|
434
|
-
function setTtl(value: number) {
|
|
435
|
-
ttl = value
|
|
436
|
-
}
|
|
437
|
-
function getTtl() {
|
|
438
|
-
return ttl
|
|
439
|
-
}
|
|
440
666
|
|
|
441
|
-
function
|
|
442
|
-
|
|
667
|
+
function getDestroyListeners() {
|
|
668
|
+
return Array.from(destroyListeners)
|
|
443
669
|
}
|
|
444
670
|
|
|
445
671
|
return {
|
|
446
672
|
// @ts-expect-error This is correct type
|
|
447
673
|
async inject(token, args) {
|
|
448
|
-
|
|
449
|
-
if (
|
|
450
|
-
|
|
674
|
+
// 1. Check RequestContextHolder first (if provided and has higher priority)
|
|
675
|
+
if (contextHolder && contextHolder.priority > 0) {
|
|
676
|
+
const instanceName = self.generateInstanceName(token, args)
|
|
677
|
+
const prePreparedInstance = contextHolder.getInstance(instanceName)
|
|
678
|
+
if (prePreparedInstance !== undefined) {
|
|
679
|
+
self.logger?.debug(
|
|
680
|
+
`[ServiceLocator] Using pre-prepared instance ${instanceName} from request context ${contextHolder.requestId}`,
|
|
681
|
+
)
|
|
682
|
+
deps.add(instanceName)
|
|
683
|
+
return prePreparedInstance
|
|
684
|
+
}
|
|
451
685
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
686
|
+
|
|
687
|
+
// 2. Check current request context (if different from provided contextHolder)
|
|
688
|
+
if (
|
|
689
|
+
self.currentRequestContext &&
|
|
690
|
+
self.currentRequestContext !== contextHolder
|
|
691
|
+
) {
|
|
692
|
+
const instanceName = self.generateInstanceName(token, args)
|
|
693
|
+
const prePreparedInstance =
|
|
694
|
+
self.currentRequestContext.getInstance(instanceName)
|
|
695
|
+
if (prePreparedInstance !== undefined) {
|
|
696
|
+
self.logger?.debug(
|
|
697
|
+
`[ServiceLocator] Using pre-prepared instance ${instanceName} from current request context ${self.currentRequestContext.requestId}`,
|
|
698
|
+
)
|
|
699
|
+
deps.add(instanceName)
|
|
700
|
+
return prePreparedInstance
|
|
459
701
|
}
|
|
460
|
-
const instanceName = self.makeInstanceName(token, validatedArgs)
|
|
461
|
-
dependencies.add(instanceName)
|
|
462
|
-
return self.getOrThrowInstance(injectionToken, args as any)
|
|
463
702
|
}
|
|
464
|
-
|
|
465
|
-
|
|
703
|
+
|
|
704
|
+
// 3. Fall back to normal resolution
|
|
705
|
+
const [error, instance] = await self.getInstance(
|
|
706
|
+
token,
|
|
707
|
+
args,
|
|
708
|
+
({ instanceName }) => {
|
|
709
|
+
deps.add(instanceName)
|
|
710
|
+
},
|
|
466
711
|
)
|
|
712
|
+
if (error) {
|
|
713
|
+
throw error
|
|
714
|
+
}
|
|
715
|
+
return instance
|
|
467
716
|
},
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
getDependencies: () => Array.from(dependencies),
|
|
471
|
-
addEffect,
|
|
472
|
-
getDestroyListeners: () => Array.from(destroyListeners),
|
|
473
|
-
setTtl,
|
|
474
|
-
getTtl,
|
|
717
|
+
addDestroyListener,
|
|
718
|
+
getDestroyListeners,
|
|
475
719
|
locator: self,
|
|
720
|
+
deps,
|
|
476
721
|
}
|
|
477
722
|
}
|
|
478
723
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
token: InjectionToken<Instance, Schema>,
|
|
484
|
-
args: Schema extends ZodObject<any>
|
|
485
|
-
? z.input<Schema>
|
|
486
|
-
: Schema extends ZodOptional<ZodObject<any>>
|
|
487
|
-
? z.input<Schema> | undefined
|
|
488
|
-
: undefined,
|
|
489
|
-
): Instance | null {
|
|
490
|
-
const [err, realArgs] = this.resolveTokenArgs(token, args)
|
|
491
|
-
if (err) {
|
|
492
|
-
return null
|
|
493
|
-
}
|
|
494
|
-
const instanceName = this.makeInstanceName(token, realArgs)
|
|
495
|
-
const [error, holder] = this.manager.get(instanceName)
|
|
496
|
-
if (error) {
|
|
497
|
-
return null
|
|
498
|
-
}
|
|
499
|
-
return holder.instance as Instance
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
invalidate(service: string, round = 1): Promise<any> {
|
|
503
|
-
this.logger?.log(
|
|
504
|
-
`[ServiceLocator]#invalidate(): Starting Invalidating process of ${service}`,
|
|
505
|
-
)
|
|
506
|
-
const toInvalidate = this.manager.filter(
|
|
507
|
-
(holder) => holder.name === service || holder.deps.includes(service),
|
|
508
|
-
)
|
|
509
|
-
const promises = []
|
|
510
|
-
for (const [key, holder] of toInvalidate.entries()) {
|
|
511
|
-
if (holder.status === ServiceLocatorInstanceHolderStatus.Destroying) {
|
|
512
|
-
this.logger?.trace(
|
|
513
|
-
`[ServiceLocator]#invalidate(): ${key} is already being destroyed`,
|
|
514
|
-
)
|
|
515
|
-
promises.push(holder.destroyPromise)
|
|
516
|
-
continue
|
|
517
|
-
}
|
|
518
|
-
if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
|
|
519
|
-
this.logger?.trace(
|
|
520
|
-
`[ServiceLocator]#invalidate(): ${key} is being created, waiting for creation to finish`,
|
|
521
|
-
)
|
|
522
|
-
promises.push(
|
|
523
|
-
holder.creationPromise?.then(() => {
|
|
524
|
-
if (round > 3) {
|
|
525
|
-
this.logger?.error(
|
|
526
|
-
`[ServiceLocator]#invalidate(): ${key} creation is triggering a new invalidation round, but it is still not created`,
|
|
527
|
-
)
|
|
528
|
-
return
|
|
529
|
-
}
|
|
530
|
-
return this.invalidate(key, round + 1)
|
|
531
|
-
}),
|
|
532
|
-
)
|
|
533
|
-
continue
|
|
534
|
-
}
|
|
535
|
-
// @ts-expect-error TS2322 we are changing the status
|
|
536
|
-
holder.status = ServiceLocatorInstanceHolderStatus.Destroying
|
|
537
|
-
this.logger?.log(
|
|
538
|
-
`[ServiceLocator]#invalidate(): Invalidating ${key} and notifying listeners`,
|
|
539
|
-
)
|
|
540
|
-
// @ts-expect-error TS2322 we are changing the status
|
|
541
|
-
holder.destroyPromise = Promise.all(
|
|
542
|
-
holder.destroyListeners.map((listener) => listener()),
|
|
543
|
-
).then(async () => {
|
|
544
|
-
this.manager.delete(key)
|
|
545
|
-
await this.notifyListeners(key, 'destroy')
|
|
546
|
-
})
|
|
547
|
-
promises.push(holder.destroyPromise)
|
|
548
|
-
}
|
|
549
|
-
return Promise.all(promises)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
async ready() {
|
|
553
|
-
return Promise.all(
|
|
554
|
-
Array.from(this.manager.filter(() => true)).map(([, holder]) => {
|
|
555
|
-
if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
|
|
556
|
-
return holder.creationPromise?.then(() => null)
|
|
557
|
-
}
|
|
558
|
-
if (holder.status === ServiceLocatorInstanceHolderStatus.Destroying) {
|
|
559
|
-
return holder.destroyPromise.then(() => null)
|
|
560
|
-
}
|
|
561
|
-
return Promise.resolve(null)
|
|
562
|
-
}),
|
|
563
|
-
).then(() => null)
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
makeInstanceName(
|
|
567
|
-
token:
|
|
568
|
-
| InjectionToken<any, any>
|
|
569
|
-
| BoundInjectionToken<any, any>
|
|
570
|
-
| FactoryInjectionToken<any, any>,
|
|
571
|
-
args: any,
|
|
572
|
-
) {
|
|
724
|
+
/**
|
|
725
|
+
* Generates a unique instance name based on token and arguments.
|
|
726
|
+
*/
|
|
727
|
+
private generateInstanceName(token: InjectionTokenType, args: any) {
|
|
573
728
|
const formattedArgs = args
|
|
574
729
|
? ':' +
|
|
575
730
|
Object.entries(args ?? {})
|