@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.
Files changed (62) hide show
  1. package/README.md +301 -39
  2. package/docs/README.md +122 -49
  3. package/docs/api-reference.md +763 -0
  4. package/docs/container.md +274 -0
  5. package/docs/examples/basic-usage.mts +97 -0
  6. package/docs/examples/factory-pattern.mts +318 -0
  7. package/docs/examples/injection-tokens.mts +225 -0
  8. package/docs/examples/request-scope-example.mts +254 -0
  9. package/docs/examples/service-lifecycle.mts +359 -0
  10. package/docs/factory.md +584 -0
  11. package/docs/getting-started.md +308 -0
  12. package/docs/injectable.md +496 -0
  13. package/docs/injection-tokens.md +400 -0
  14. package/docs/lifecycle.md +539 -0
  15. package/docs/scopes.md +749 -0
  16. package/lib/_tsup-dts-rollup.d.mts +495 -150
  17. package/lib/_tsup-dts-rollup.d.ts +495 -150
  18. package/lib/index.d.mts +26 -12
  19. package/lib/index.d.ts +26 -12
  20. package/lib/index.js +993 -462
  21. package/lib/index.js.map +1 -1
  22. package/lib/index.mjs +983 -453
  23. package/lib/index.mjs.map +1 -1
  24. package/package.json +2 -2
  25. package/project.json +10 -2
  26. package/src/__tests__/container.spec.mts +1301 -0
  27. package/src/__tests__/factory.spec.mts +137 -0
  28. package/src/__tests__/injectable.spec.mts +32 -88
  29. package/src/__tests__/injection-token.spec.mts +333 -17
  30. package/src/__tests__/request-scope.spec.mts +263 -0
  31. package/src/__type-tests__/factory.spec-d.mts +65 -0
  32. package/src/__type-tests__/inject.spec-d.mts +27 -28
  33. package/src/__type-tests__/injectable.spec-d.mts +42 -206
  34. package/src/container.mts +167 -0
  35. package/src/decorators/factory.decorator.mts +79 -0
  36. package/src/decorators/index.mts +1 -0
  37. package/src/decorators/injectable.decorator.mts +6 -56
  38. package/src/enums/injectable-scope.enum.mts +5 -1
  39. package/src/event-emitter.mts +18 -20
  40. package/src/factory-context.mts +2 -10
  41. package/src/index.mts +3 -2
  42. package/src/injection-token.mts +24 -9
  43. package/src/injector.mts +8 -20
  44. package/src/interfaces/factory.interface.mts +3 -3
  45. package/src/interfaces/index.mts +2 -0
  46. package/src/interfaces/on-service-destroy.interface.mts +3 -0
  47. package/src/interfaces/on-service-init.interface.mts +3 -0
  48. package/src/registry.mts +7 -16
  49. package/src/request-context-holder.mts +145 -0
  50. package/src/service-instantiator.mts +158 -0
  51. package/src/service-locator-event-bus.mts +0 -28
  52. package/src/service-locator-instance-holder.mts +27 -16
  53. package/src/service-locator-manager.mts +84 -0
  54. package/src/service-locator.mts +550 -395
  55. package/src/utils/defer.mts +73 -0
  56. package/src/utils/get-injectors.mts +93 -80
  57. package/src/utils/index.mts +2 -0
  58. package/src/utils/types.mts +52 -0
  59. package/docs/concepts/injectable.md +0 -182
  60. package/docs/concepts/injection-token.md +0 -145
  61. package/src/proxy-service-locator.mts +0 -83
  62. package/src/resolve-service.mts +0 -41
@@ -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
- public storeInstance<Instance>(
52
- instance: Instance,
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 removeInstance<Instance>(
98
- token: BoundInjectionToken<Instance, any>,
99
- ): void
100
- public removeInstance<Instance>(
101
- token: FactoryInjectionToken<Instance, any>,
102
- ): void
103
- public removeInstance<Instance>(
104
- token: InjectionToken<Instance, undefined>,
105
- ): void
106
- public removeInstance<Instance, Schema extends BaseInjectionTokenSchemaType>(
107
- token: InjectionToken<Instance, Schema>,
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
- // @ts-expect-error We should redefine the instance name type
123
- const instanceName = this.getInstanceIdentifier(token, args)
124
- return this.invalidate(instanceName)
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
- private resolveTokenArgs<
128
- Instance,
129
- Schema extends BaseInjectionTokenSchemaType,
130
- >(
131
- token: InjectionToken<Instance, Schema>,
132
- args: z.input<Schema>,
133
- ): [undefined, z.output<Schema>] | [UnknownError]
134
- private resolveTokenArgs<
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 OptionalInjectionTokenSchemaType,
121
+ Schema extends InjectionTokenSchemaType | undefined,
137
122
  >(
138
- token: InjectionToken<Instance, Schema>,
139
- args?: z.input<Schema>,
140
- ): [undefined, z.output<Schema>] | [UnknownError]
141
- private resolveTokenArgs<Instance, Schema extends InjectionTokenSchemaType>(
142
- token: BoundInjectionToken<Instance, Schema>,
143
- ): [undefined, z.output<Schema>] | [UnknownError]
144
- private resolveTokenArgs<Instance, Schema extends InjectionTokenSchemaType>(
145
- token: FactoryInjectionToken<Instance, Schema>,
146
- ): [undefined, z.output<Schema>] | [FactoryTokenNotResolved | UnknownError]
147
- private resolveTokenArgs(
148
- token:
149
- | InjectionToken<any, any>
150
- | BoundInjectionToken<any, any>
151
- | FactoryInjectionToken<any, any>,
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 (token instanceof BoundInjectionToken) {
156
- realArgs = token.value
157
- } else if (token instanceof FactoryInjectionToken) {
158
- if (token.resolved) {
159
- realArgs = token.value
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 (!token.schema) {
165
- return [undefined, realArgs]
335
+ if (!actualToken.schema) {
336
+ return [undefined, { actualToken, validatedArgs: realArgs }]
166
337
  }
167
- const validatedArgs = token.schema?.safeParse(realArgs)
338
+ const validatedArgs = actualToken.schema?.safeParse(realArgs)
168
339
  if (validatedArgs && !validatedArgs.success) {
169
340
  this.logger?.error(
170
- `[ServiceLocator]#resolveTokenArgs(): Error validating args for ${token.name.toString()}`,
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
- public getInstanceIdentifier<
179
- Instance,
180
- Schema extends BaseInjectionTokenSchemaType,
181
- >(token: InjectionToken<Instance, Schema>, args: z.input<Schema>): string
182
- public getInstanceIdentifier<
183
- Instance,
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
- const [err, realArgs] = this.resolveTokenArgs(token as any, args)
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
- token instanceof FactoryInjectionToken
374
+ actualToken instanceof FactoryInjectionToken
243
375
  ) {
244
376
  this.logger?.log(
245
- `[ServiceLocator]#getInstance() Factory token not resolved, resolving it`,
377
+ `[ServiceLocator]#resolveTokenAndPrepareInstanceName() Factory token not resolved, resolving it`,
246
378
  )
247
- await token.resolve()
248
- return this.getInstance(token)
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
- const instanceName = this.makeInstanceName(token, realArgs)
434
+
251
435
  const [error, holder] = this.manager.get(instanceName)
252
436
  if (!error) {
253
437
  if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
254
- return holder.creationPromise
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.instance]
450
+ return [undefined, holder]
262
451
  }
263
452
  switch (error.code) {
264
453
  case ErrorsEnum.InstanceDestroying:
265
454
  this.logger?.log(
266
- `[ServiceLocator]#getInstance() TTL expired for ${holder?.name}`,
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
- // @ts-expect-error We should redefine the instance name type
271
- return this.getInstance(token, args)
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]#getInstance() TTL expired for ${holder?.name}`,
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
- // @ts-expect-error We should redefine the instance name type
280
- return this.getInstance(token, args)
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
- // @ts-expect-error TS2322 It's validated
287
- return this.createInstance(instanceName, token, realArgs)
288
- }
289
-
290
- public async getOrThrowInstance<
291
- Instance,
292
- Schema extends InjectionTokenSchemaType | undefined,
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
- return instance
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
- private notifyListeners(
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]#notifyListeners() Notifying listeners for ${name} with event ${event}`,
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
- private async createInstance<
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
- token: InjectionToken<Instance, Schema>,
324
- args: Schema extends ZodObject<any>
325
- ? z.input<Schema>
326
- : Schema extends ZodOptional<ZodObject<any>>
327
- ? z.input<Schema> | undefined
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<[undefined, Instance] | [FactoryNotFound | UnknownError]> {
525
+ ): Promise<
526
+ | [undefined, ServiceLocatorInstanceHolder<Instance>]
527
+ | [FactoryNotFound | UnknownError]
528
+ > {
330
529
  this.logger?.log(
331
- `[ServiceLocator]#createInstance() Creating instance for ${instanceName}`,
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.resolveInstance(instanceName, realToken, args)
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
- private resolveInstance<
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.input<Schema>
550
+ ? z.output<Schema>
350
551
  : Schema extends OptionalInjectionTokenSchemaType
351
- ? z.input<Schema> | undefined
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] | [FactoryNotFound]> {
558
+ ): Promise<[undefined, ServiceLocatorInstanceHolder<Instance>]> {
358
559
  this.logger?.log(
359
- `[ServiceLocator]#resolveInstance(): Creating instance for ${instanceName} from abstract factory`,
560
+ `[ServiceLocator]#instantiateServiceFromRegistry(): Creating instance for ${instanceName} from abstract factory`,
561
+ )
562
+ const ctx = this.createFactoryContext(
563
+ this.currentRequestContext || undefined,
360
564
  )
361
- const ctx = this.createFactoryContext(instanceName)
362
- let { factory, scope } = this.registry.get<Instance, Schema>(token)
363
- const holder: ServiceLocatorInstanceHolder<Instance> = {
364
- name: instanceName,
365
- instance: null,
366
- status: ServiceLocatorInstanceHolderStatus.Creating,
367
- kind: ServiceLocatorInstanceHolderKind.AbstractFactory,
368
- // @ts-expect-error TS2322 This is correct type
369
- creationPromise: factory(ctx, args)
370
- .then(async (instance: Instance) => {
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
- holder.deps = ctx.getDependencies()
374
- holder.destroyListeners = ctx.getDestroyListeners()
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
- if (holder.ttl === 0 || scope === InjectableScope.Instance) {
391
- // One time instance
392
- await this.invalidate(instanceName)
393
- }
394
- await this.notifyListeners(instanceName)
395
- return [undefined, instance as Instance]
396
- })
397
- .catch((error) => {
398
- this.logger?.error(
399
- `[ServiceLocator]#createInstanceFromAbstractFactory(): Error creating instance for ${instanceName}`,
400
- error,
401
- )
402
- return [new UnknownError(error)]
403
- }),
404
- effects: [],
405
- deps: [],
406
- destroyListeners: [],
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]#resolveInstance(): Setting instance for ${instanceName}`,
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.creationPromise
645
+ return [undefined, holder]
419
646
  }
420
647
 
421
- private createFactoryContext(instanceName: string): FactoryContext {
422
- const dependencies = new Set<string>()
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 invalidate(name = instanceName) {
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 on(key: string, event: string, listener: (event: string) => void) {
442
- destroyListeners.add(self.eventBus.on(key, event, listener))
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
- let injectionToken = token
449
- if (typeof token === 'function') {
450
- injectionToken = getInjectableToken(token)
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
- if (injectionToken) {
453
- const [err, validatedArgs] = self.resolveTokenArgs(
454
- injectionToken,
455
- args,
456
- )
457
- if (err) {
458
- throw err
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
- throw new Error(
465
- `[ServiceLocator]#inject(): Invalid token type: ${typeof token}. Expected a class or an InjectionToken.`,
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
- invalidate,
469
- on: on as ServiceLocatorEventBus['on'],
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
- public getSyncInstance<
480
- Instance,
481
- Schema extends InjectionTokenSchemaType | undefined,
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 ?? {})