@declaro/core 2.0.0-beta.99 → 2.1.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 (60) hide show
  1. package/dist/browser/index.js +21 -27
  2. package/dist/browser/index.js.map +37 -27
  3. package/dist/browser/scope/index.js +1 -2
  4. package/dist/browser/scope/index.js.map +1 -1
  5. package/dist/bun/index.js +19011 -0
  6. package/dist/bun/index.js.map +132 -0
  7. package/dist/bun/scope/index.js +4 -0
  8. package/dist/bun/scope/index.js.map +9 -0
  9. package/dist/node/index.cjs +2581 -874
  10. package/dist/node/index.cjs.map +36 -27
  11. package/dist/node/index.js +2572 -868
  12. package/dist/node/index.js.map +36 -27
  13. package/dist/node/scope/index.cjs +31 -10
  14. package/dist/node/scope/index.cjs.map +1 -1
  15. package/dist/node/scope/index.js +1 -27
  16. package/dist/node/scope/index.js.map +1 -1
  17. package/dist/ts/context/async-context.d.ts +54 -0
  18. package/dist/ts/context/async-context.d.ts.map +1 -0
  19. package/dist/ts/context/async-context.test.d.ts +2 -0
  20. package/dist/ts/context/async-context.test.d.ts.map +1 -0
  21. package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
  22. package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
  23. package/dist/ts/context/context.d.ts +297 -38
  24. package/dist/ts/context/context.d.ts.map +1 -1
  25. package/dist/ts/http/request-context.d.ts.map +1 -1
  26. package/dist/ts/index.d.ts +2 -0
  27. package/dist/ts/index.d.ts.map +1 -1
  28. package/dist/ts/schema/json-schema.d.ts +9 -1
  29. package/dist/ts/schema/json-schema.d.ts.map +1 -1
  30. package/dist/ts/schema/model.d.ts +6 -1
  31. package/dist/ts/schema/model.d.ts.map +1 -1
  32. package/dist/ts/schema/test/mock-model.d.ts +2 -2
  33. package/dist/ts/schema/test/mock-model.d.ts.map +1 -1
  34. package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
  35. package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
  36. package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
  37. package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
  38. package/dist/ts/shims/async-local-storage.d.ts +36 -0
  39. package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
  40. package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
  41. package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
  42. package/package.json +17 -9
  43. package/src/context/async-context.test.ts +348 -0
  44. package/src/context/async-context.ts +129 -0
  45. package/src/context/context.circular-deps.test.ts +1047 -0
  46. package/src/context/context.test.ts +150 -0
  47. package/src/context/context.ts +493 -55
  48. package/src/http/request-context.ts +1 -3
  49. package/src/index.ts +2 -0
  50. package/src/schema/json-schema.ts +14 -1
  51. package/src/schema/model-schema.test.ts +155 -1
  52. package/src/schema/model.ts +34 -3
  53. package/src/schema/test/mock-model.ts +6 -2
  54. package/src/shared/utils/schema-utils.test.ts +33 -0
  55. package/src/shared/utils/schema-utils.ts +17 -0
  56. package/src/shims/async-local-storage.test.ts +258 -0
  57. package/src/shims/async-local-storage.ts +82 -0
  58. package/dist/ts/schema/entity-schema.test.d.ts +0 -1
  59. package/dist/ts/schema/entity-schema.test.d.ts.map +0 -1
  60. package/src/schema/entity-schema.test.ts +0 -0
@@ -5,19 +5,39 @@ import type { AllNodeMiddleware } from '../http/request-context'
5
5
  import type { Class, PromiseOrValue, UnwrapPromise } from '../typescript'
6
6
  import { validate, validateAny, type Validator } from '../validation'
7
7
  import { ContextConsumer } from './context-consumer'
8
+ import { useContext, withContext } from './async-context'
8
9
 
10
+ /**
11
+ * Global interface for declaring dependencies available across all contexts.
12
+ * Extend this interface using declaration merging to add type-safe dependencies.
13
+ */
9
14
  export interface DeclaroDependencies {}
10
15
 
16
+ /**
17
+ * Base scope interface for contexts with request and node middleware.
18
+ */
11
19
  export interface DeclaroScope {
20
+ /** Middleware that runs on request contexts */
12
21
  requestMiddleware: ContextMiddleware<Context>[]
22
+ /** Node.js-compatible middleware */
13
23
  nodeMiddleware: AllNodeMiddleware[]
14
24
  }
25
+
26
+ /**
27
+ * Scope interface for request-specific contexts, extending the base scope with request data.
28
+ */
15
29
  export interface DeclaroRequestScope extends DeclaroScope {
30
+ /** The HTTP request object */
16
31
  request: Request
32
+ /** Incoming HTTP headers */
17
33
  headers: IncomingHttpHeaders
34
+ /** Helper function to retrieve a specific header value */
18
35
  header: <K extends keyof IncomingHttpHeaders>(header: K) => IncomingHttpHeaders[K] | undefined
19
36
  }
20
37
 
38
+ /**
39
+ * Extracts the scope type from a Context type.
40
+ */
21
41
  export type ExtractScope<T extends Context<any>> = T extends Context<infer S> ? S : never
22
42
 
23
43
  /**
@@ -32,53 +52,194 @@ export type NarrowContext<TContext extends Context<any>, TNarrowScope extends ob
32
52
  : never
33
53
  : never
34
54
 
55
+ /**
56
+ * Middleware function that can modify or extend a context.
57
+ */
35
58
  export type ContextMiddleware<C extends Context = Context> = (context: Context<ExtractScope<C>>) => any | Promise<any>
59
+
60
+ /**
61
+ * Represents the state storage for a context, mapping keys to their attributes.
62
+ */
36
63
  export type ContextState<TContext extends Context> = Record<PropertyKey, ContextAttribute<TContext, StateValue<any>>>
37
64
 
65
+ /**
66
+ * Function that resolves a value from a context.
67
+ */
38
68
  export type ContextResolver<T> = (context: Context) => StateValue<T>
39
69
 
70
+ /**
71
+ * Wrapper type for state values.
72
+ */
40
73
  export type StateValue<T> = T
41
74
 
75
+ /**
76
+ * Types of dependencies that can be registered in a context.
77
+ */
42
78
  export enum DependencyType {
79
+ /** A literal value dependency */
43
80
  VALUE = 'VALUE',
81
+ /** A factory function that creates the dependency */
44
82
  FACTORY = 'FACTORY',
83
+ /** A class constructor that instantiates the dependency */
45
84
  CLASS = 'CLASS',
46
85
  }
47
86
 
87
+ /**
88
+ * Factory function type that takes arguments and returns a value.
89
+ */
48
90
  export type FactoryFn<T, A extends any[]> = (...args: A) => T
49
- export type ValueLoader<C extends Context, T> = (context: C) => T
91
+
92
+ /**
93
+ * Function that loads a value from a context with optional resolution options.
94
+ */
95
+ export type ValueLoader<C extends Context, T> = (context: C, resolutionOptions?: ResolveOptions) => T
96
+
97
+ /**
98
+ * Filters object keys to only those whose values match a specific type.
99
+ */
50
100
  export type FilterKeysByType<TScope, TValue> = {
51
101
  [Key in keyof TScope]: TScope[Key] extends TValue ? Key : never
52
102
  }[keyof TScope]
103
+
104
+ /**
105
+ * Filters object keys to only those whose values match a specific type or Promise of that type.
106
+ */
53
107
  export type FilterKeysByAsyncType<TScope, TValue> = {
54
108
  [Key in keyof TScope]: TScope[Key] extends PromiseOrValue<TValue> ? Key : never
55
109
  }[keyof TScope]
110
+
111
+ /**
112
+ * Maps an array of argument types to their corresponding scope keys.
113
+ */
56
114
  export type FilterArgsByType<TScope, TArgs extends any[]> = {
57
115
  [Key in keyof TArgs]: FilterKeysByType<TScope, TArgs[Key]>
58
116
  }
117
+
118
+ /**
119
+ * Maps an array of argument types to their corresponding scope keys for async values.
120
+ */
59
121
  export type FilterAsyncArgsByType<TScope, TArgs extends any[]> = {
60
122
  [Key in keyof TArgs]: FilterKeysByAsyncType<TScope, TArgs[Key]>
61
123
  }
62
124
 
125
+ /**
126
+ * Metadata describing how a dependency is registered and resolved in a context.
127
+ */
63
128
  export type ContextAttribute<TContext extends Context<any>, TValue> = {
129
+ /** The key under which this dependency is registered */
64
130
  key: PropertyKey
131
+ /** Function that loads the value from the context */
65
132
  value?: ValueLoader<TContext, TValue>
133
+ /** The type of dependency (value, factory, or class) */
66
134
  type: DependencyType
135
+ /** Options controlling how this dependency is resolved */
67
136
  resolveOptions?: ResolveOptions
137
+ /** Cached value for singleton or eager dependencies */
68
138
  cachedValue?: TValue
139
+ /** Keys of other dependencies that this dependency requires */
69
140
  inject: PropertyKey[]
70
141
  }
71
142
 
143
+ /**
144
+ * Type-safe scope key extraction.
145
+ */
72
146
  export type ScopeKey<S extends object> = keyof S
73
147
 
148
+ /**
149
+ * Listener function that responds to events in a context.
150
+ */
74
151
  export type ContextListener<C extends Context, E extends IEvent> = (context: C, event: E) => any
75
152
 
76
- export type ResolveOptions = {
153
+ /**
154
+ * Interface for circular dependency proxies that defer to a real target once resolved.
155
+ * These proxies are created during circular dependency resolution to allow references
156
+ * to objects that haven't been fully constructed yet.
157
+ */
158
+ export interface ResolveProxy<T = any> {
159
+ /**
160
+ * Identifies this object as a circular proxy.
161
+ * @internal
162
+ */
163
+ readonly __isProxy: true
164
+
165
+ /**
166
+ * Sets the real target object that this proxy should delegate to.
167
+ * Called internally once the circular dependency is resolved.
168
+ * @internal
169
+ */
170
+ readonly __resolve: (target: T) => void
171
+
172
+ /** Indicates whether the proxy has been resolved to a real target. */
173
+ readonly __isResolved: boolean
174
+
175
+ /**
176
+ * Returns the real target object if resolved, otherwise returns the proxy itself.
177
+ * This allows using the proxy transparently before and after resolution.
178
+ */
179
+ readonly valueOf: () => T
180
+ }
181
+
182
+ /**
183
+ * Type guard to check if a value is a circular dependency proxy.
184
+ *
185
+ * @param value - The value to check
186
+ * @returns True if the value is a ResolveProxy
187
+ */
188
+ export function isProxy(value: any): value is ResolveProxy {
189
+ return value && typeof value === 'object' && value.__isProxy === true
190
+ }
191
+
192
+ export interface ResolveOptions {
193
+ /**
194
+ * If true, an error will be thrown if the dependency is not found. If false, undefined will be returned if the dependency is not found.
195
+ * @default false
196
+ */
77
197
  strict?: boolean
198
+
199
+ /**
200
+ * If true, the dependency will be resolved immediately when the context is initialized. This is useful for dependencies that need to perform setup work.
201
+ * @default false
202
+ */
78
203
  eager?: boolean
204
+
205
+ /**
206
+ * If true, the dependency will be a singleton, and the same instance will be returned for every request. If false, a new instance will be created each time the dependency is resolved.
207
+ * @default false
208
+ */
79
209
  singleton?: boolean
210
+
211
+ /**
212
+ * An optional resolution context that can be used to track resolution state across multiple `resolve` calls. This is primarily used internally to track circular dependencies, but can be useful for advanced use cases.
213
+ */
214
+ resolutionContext?: Map<PropertyKey, any>
215
+ }
216
+
217
+ /**
218
+ * Extracts nested resolution options, preserving resolution state across calls.
219
+ *
220
+ * @param options - The resolve options to extract from
221
+ * @returns Internal resolution options with resolution stack and context
222
+ */
223
+ export function getNestedResolveOptions(options?: ResolveOptions | InternalResolveOptions): InternalResolveOptions {
224
+ return {
225
+ resolutionStack: (options as InternalResolveOptions)?.resolutionStack,
226
+ resolutionContext: options?.resolutionContext,
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Internal interface extending ResolveOptions with resolution tracking.
232
+ */
233
+ export interface InternalResolveOptions extends ResolveOptions {
234
+ /** Stack tracking the current resolution chain to detect circular dependencies */
235
+ resolutionStack: Set<PropertyKey>
80
236
  }
81
237
 
238
+ /**
239
+ * Returns the default resolution options for dependencies.
240
+ *
241
+ * @returns Default resolve options with strict, eager, and singleton all set to false
242
+ */
82
243
  export function defaultResolveOptions(): ResolveOptions {
83
244
  return {
84
245
  strict: false,
@@ -87,18 +248,43 @@ export function defaultResolveOptions(): ResolveOptions {
87
248
  }
88
249
  }
89
250
 
251
+ /**
252
+ * Helper function to define type-safe context middleware.
253
+ *
254
+ * @param middleware - The middleware function to define
255
+ * @returns The same middleware function with proper typing
256
+ */
257
+ export function defineContextMiddleware<C extends Context>(middleware: ContextMiddleware<C>): ContextMiddleware<C> {
258
+ return middleware
259
+ }
260
+
261
+ /**
262
+ * Configuration options for creating a context.
263
+ */
90
264
  export type ContextOptions = {
265
+ /** Default options to use when resolving dependencies */
91
266
  defaultResolveOptions?: ResolveOptions
92
267
  }
93
268
 
269
+ /**
270
+ * Core dependency injection container that manages application dependencies and their lifecycle.
271
+ * Supports values, factories, and classes with automatic dependency resolution and circular dependency handling.
272
+ */
94
273
  export class Context<Scope extends object = any> {
95
274
  private readonly state: ContextState<this> = {}
96
275
  private readonly emitter = new EventManager()
97
276
 
277
+ /** The scope object providing typed access to all registered dependencies */
98
278
  public readonly scope: Scope = {} as Scope
99
279
 
280
+ /** Default options used when resolving dependencies if not overridden */
100
281
  protected readonly defaultResolveOptions: ResolveOptions
101
282
 
283
+ /**
284
+ * Creates a new context instance.
285
+ *
286
+ * @param options - Configuration options for the context
287
+ */
102
288
  constructor(options?: ContextOptions) {
103
289
  this.defaultResolveOptions = {
104
290
  ...defaultResolveOptions(),
@@ -106,10 +292,19 @@ export class Context<Scope extends object = any> {
106
292
  }
107
293
  }
108
294
 
295
+ /**
296
+ * Gets the event manager for this context.
297
+ *
298
+ * @returns The event manager instance
299
+ */
109
300
  get events() {
110
301
  return this.emitter
111
302
  }
112
303
 
304
+ /**
305
+ * Initializes all dependencies marked as eager.
306
+ * Should be called after all dependencies are registered to trigger eager initialization.
307
+ */
113
308
  async initializeEagerDependencies() {
114
309
  await Promise.all(
115
310
  Object.entries(this.state)
@@ -123,8 +318,8 @@ export class Context<Scope extends object = any> {
123
318
  /**
124
319
  * Set a value in context, to be injected later.
125
320
  *
126
- * @param key
127
- * @param payload
321
+ * @param key - The scope key to register the value under
322
+ * @param payload - The value to register
128
323
  * @deprecated Use `provideValue` instead, or you can register the same dependency as a factory with `provideFactory` or class with `provideClass`.
129
324
  */
130
325
  provide<K extends ScopeKey<Scope>>(key: K, payload: Scope[K]) {
@@ -165,6 +360,10 @@ export class Context<Scope extends object = any> {
165
360
 
166
361
  /**
167
362
  * Add a dependency to the context.
363
+ *
364
+ * @param key - The scope key to register under
365
+ * @param dep - The dependency attribute to add
366
+ * @returns The registered dependency attribute
168
367
  */
169
368
  protected addDep<K extends ScopeKey<Scope>>(key: K, dep: ContextAttribute<this, Scope[K]>) {
170
369
  this.state[key] = dep
@@ -181,8 +380,10 @@ export class Context<Scope extends object = any> {
181
380
  /**
182
381
  * Register a value in context scope.
183
382
  *
184
- * @param key The key to register the dependency under
185
- * @param value The value to register
383
+ * @param key - The key to register the dependency under
384
+ * @param value - The value to register
385
+ * @param defaultResolveOptions - Optional resolution options
386
+ * @returns The context instance for chaining
186
387
  */
187
388
  registerValue<K extends ScopeKey<Scope>>(key: K, value: Scope[K], defaultResolveOptions?: ResolveOptions) {
188
389
  const attribute: ContextAttribute<this, Scope[K]> = {
@@ -201,9 +402,10 @@ export class Context<Scope extends object = any> {
201
402
  /**
202
403
  * Register a dependency as a factory in context scope.
203
404
  *
204
- * @param key The key to register the dependency under
205
- * @param factory A factory function that will be called to generate the value when it is requested.
206
- * @param inject An array of keys to use when injecting factory args.
405
+ * @param key - The key to register the dependency under
406
+ * @param factory - A factory function that will be called to generate the value when it is requested
407
+ * @param inject - An array of keys to use when injecting factory args
408
+ * @param defaultResolveOptions - Optional resolution options
207
409
  * @returns A chainable instance of context
208
410
  */
209
411
  registerFactory<K extends ScopeKey<Scope>, A extends any[]>(
@@ -213,8 +415,10 @@ export class Context<Scope extends object = any> {
213
415
  defaultResolveOptions?: ResolveOptions,
214
416
  ) {
215
417
  const attribute: ContextAttribute<this, Scope[K]> = {
216
- value: (context) => {
217
- const args = (inject?.map((key) => context.resolve(key)) ?? []) as A
418
+ value: (context, resolveOptions) => {
419
+ const args = (inject?.map((key) =>
420
+ context._resolveValue(key, getNestedResolveOptions(resolveOptions)),
421
+ ) ?? []) as A
218
422
 
219
423
  return factory(...args)
220
424
  },
@@ -229,6 +433,16 @@ export class Context<Scope extends object = any> {
229
433
  return this
230
434
  }
231
435
 
436
+ /**
437
+ * Register an async factory in context scope.
438
+ * The factory function and its dependencies can be asynchronous.
439
+ *
440
+ * @param key - The key to register the dependency under
441
+ * @param factory - An async factory function that will be called to generate the value
442
+ * @param inject - An array of keys to use when injecting factory args
443
+ * @param defaultResolveOptions - Optional resolution options
444
+ * @returns A chainable instance of context
445
+ */
232
446
  registerAsyncFactory<K extends FilterKeysByType<Scope, Promise<any>>, A extends any[]>(
233
447
  key: K,
234
448
  factory: FactoryFn<Scope[K], A>,
@@ -236,8 +450,10 @@ export class Context<Scope extends object = any> {
236
450
  defaultResolveOptions?: ResolveOptions,
237
451
  ) {
238
452
  const attribute: ContextAttribute<this, Scope[K]> = {
239
- value: (async (context) => {
240
- const args = (await Promise.all((inject?.map((key) => context.resolve(key)) as A) ?? [])) as A
453
+ value: (async (context, resolveOptions) => {
454
+ const args = (await Promise.all(
455
+ (inject?.map((key) => context.resolve(key, getNestedResolveOptions(resolveOptions))) as A) ?? [],
456
+ )) as A
241
457
 
242
458
  return await factory(...args)
243
459
  }) as ValueLoader<this, Scope[K]>,
@@ -252,6 +468,16 @@ export class Context<Scope extends object = any> {
252
468
  return this
253
469
  }
254
470
 
471
+ /**
472
+ * Register a class constructor in context scope.
473
+ * The class will be instantiated with the specified dependencies.
474
+ *
475
+ * @param key - The key to register the dependency under
476
+ * @param Class - The class constructor
477
+ * @param inject - An array of keys to use when injecting constructor arguments
478
+ * @param defaultResolveOptions - Optional resolution options
479
+ * @returns A chainable instance of context
480
+ */
255
481
  registerClass<
256
482
  K extends FilterKeysByType<Scope, InstanceType<T>>,
257
483
  T extends Class<Scope[K] extends {} ? Scope[K] : never>,
@@ -262,8 +488,8 @@ export class Context<Scope extends object = any> {
262
488
  defaultResolveOptions?: ResolveOptions,
263
489
  ) {
264
490
  const attribute: ContextAttribute<this, Scope[K]> = {
265
- value: (context) => {
266
- const args = inject?.map((key) => context.resolve(key)) ?? []
491
+ value: (context, resolveOptions) => {
492
+ const args = inject?.map((key) => context.resolve(key, getNestedResolveOptions(resolveOptions))) ?? []
267
493
 
268
494
  return new (Class as any)(...(args as any))
269
495
  },
@@ -278,6 +504,16 @@ export class Context<Scope extends object = any> {
278
504
  return this
279
505
  }
280
506
 
507
+ /**
508
+ * Register an async class constructor in context scope.
509
+ * The class constructor can have async dependencies.
510
+ *
511
+ * @param key - The key to register the dependency under
512
+ * @param Class - The class constructor
513
+ * @param inject - An array of keys to use when injecting constructor arguments
514
+ * @param defaultResolveOptions - Optional resolution options
515
+ * @returns A chainable instance of context
516
+ */
281
517
  registerAsyncClass<
282
518
  K extends FilterKeysByType<Scope, InstanceType<any>>,
283
519
  T extends Class<UnwrapPromise<Scope[K]> extends {} ? UnwrapPromise<Scope[K]> : never>,
@@ -288,9 +524,10 @@ export class Context<Scope extends object = any> {
288
524
  defaultResolveOptions?: ResolveOptions,
289
525
  ) {
290
526
  const attribute: ContextAttribute<this, Scope[K]> = {
291
- value: (async (context) => {
527
+ value: (async (context, resolveOptions) => {
292
528
  const args = (await Promise.all(
293
- (inject?.map((key) => context.resolve(key)) ?? []) as ConstructorParameters<T>,
529
+ (inject?.map((key) => context.resolve(key, getNestedResolveOptions(resolveOptions))) ??
530
+ []) as ConstructorParameters<T>,
294
531
  )) as ConstructorParameters<T>
295
532
 
296
533
  return new (Class as any)(...(args as any))
@@ -306,6 +543,12 @@ export class Context<Scope extends object = any> {
306
543
  return this
307
544
  }
308
545
 
546
+ /**
547
+ * Gets all dependencies required by a specific dependency.
548
+ *
549
+ * @param key - The key of the dependency to inspect
550
+ * @returns Array of all direct and transitive dependencies
551
+ */
309
552
  getAllDependencies<K extends ScopeKey<Scope>>(key: K): ContextAttribute<this, any>[] {
310
553
  const attribute = this.state[key]
311
554
 
@@ -323,26 +566,59 @@ export class Context<Scope extends object = any> {
323
566
  return dependencies
324
567
  }
325
568
 
326
- getAllDependents<K extends ScopeKey<Scope>>(key: K): ContextAttribute<this, any>[] {
569
+ /**
570
+ * Gets all dependencies that depend on a specific dependency.
571
+ * Useful for cache invalidation when a dependency changes.
572
+ *
573
+ * @param key - The key of the dependency to inspect
574
+ * @param visited - Internal tracking set to prevent infinite recursion
575
+ * @returns Array of all direct and transitive dependents
576
+ */
577
+ getAllDependents<K extends ScopeKey<Scope>>(key: K, visited = new Set<any>()): ContextAttribute<this, any>[] {
578
+ if (visited.has(key)) {
579
+ return []
580
+ }
581
+
582
+ visited.add(key)
583
+
327
584
  const dependents = Object.entries(this.state)
328
585
  .filter(([_, attribute]) => attribute.inject?.includes(key))
329
586
  .map(([key, attribute]) => attribute)
330
587
 
331
588
  dependents.forEach((dependent) => {
332
- const nestedDependents = this.getAllDependents(dependent.key as any)
589
+ const nestedDependents = this.getAllDependents(dependent.key as any, visited)
333
590
  dependents.push(...nestedDependents)
334
591
  })
335
592
 
336
593
  return dependents
337
594
  }
338
595
 
596
+ /**
597
+ * Introspects a dependency to get its metadata.
598
+ *
599
+ * @param key - The key of the dependency to inspect
600
+ * @returns The context attribute for the dependency, or undefined if not found
601
+ */
339
602
  introspect<K extends ScopeKey<Scope>>(key: K) {
340
603
  const attribute = this.state[key]
341
604
 
342
605
  return attribute
343
606
  }
344
607
 
345
- protected _cacheIsValid<K extends ScopeKey<Scope>>(key: K): boolean {
608
+ /**
609
+ * Checks if a cached value is still valid for a dependency.
610
+ * A cache is invalid if the dependency or any of its transitive dependencies have been invalidated.
611
+ *
612
+ * @param key - The key of the dependency to check
613
+ * @param visited - Internal tracking set to prevent infinite recursion in circular dependencies
614
+ * @returns True if the cache is valid, false otherwise
615
+ */
616
+ protected _cacheIsValid<K extends ScopeKey<Scope>>(key: K, visited = new Set<PropertyKey>()): boolean {
617
+ // Prevent infinite recursion in circular dependencies
618
+ if (visited.has(key)) {
619
+ return true
620
+ }
621
+
346
622
  const attribute = this.state[key]
347
623
 
348
624
  const needsCache = attribute?.resolveOptions?.singleton || attribute?.resolveOptions?.eager
@@ -357,17 +633,123 @@ export class Context<Scope extends object = any> {
357
633
  return false
358
634
  }
359
635
 
360
- return hasCachedValue && attribute.inject?.every((key) => this._cacheIsValid(key as any))
636
+ visited.add(key)
637
+ const result = hasCachedValue && attribute.inject?.every((depKey) => this._cacheIsValid(depKey as any, visited))
638
+ visited.delete(key)
639
+
640
+ return result
361
641
  }
362
642
 
363
- protected _resolveValue<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K] {
364
- const attribute = this.state[key]
643
+ /**
644
+ * Creates a proxy object for handling circular dependencies.
645
+ * The proxy initially acts as a placeholder and is later resolved to the real target.
646
+ *
647
+ * @returns A proxy that can be resolved later
648
+ */
649
+ protected createProxy<T>(): ResolveProxy<T> {
650
+ let realTarget: any = null
651
+ let isResolved = false
652
+
653
+ const proxy = new Proxy({} as any, {
654
+ get: (target, prop, receiver) => {
655
+ if (prop === '__isProxy') {
656
+ return true
657
+ }
658
+ if (prop === '__resolve') {
659
+ return (newTarget: any) => {
660
+ realTarget = newTarget
661
+ isResolved = true
662
+ // Copy any properties that were set on the proxy to the real target
663
+ Object.keys(target).forEach((key) => {
664
+ if (!(key in newTarget)) {
665
+ newTarget[key] = target[key]
666
+ }
667
+ })
668
+ }
669
+ }
670
+
671
+ if (prop === '__isResolved') {
672
+ return isResolved
673
+ }
674
+
675
+ if (prop === 'valueOf') {
676
+ return () => realTarget ?? proxy
677
+ }
678
+
679
+ if (isResolved && realTarget) {
680
+ return Reflect.get(realTarget, prop, realTarget)
681
+ }
682
+
683
+ return Reflect.get(target, prop, receiver)
684
+ },
685
+
686
+ set: (target, prop, value, receiver) => {
687
+ if (isResolved && realTarget) {
688
+ return Reflect.set(realTarget, prop, value, realTarget)
689
+ }
690
+ return Reflect.set(target, prop, value, receiver)
691
+ },
692
+
693
+ has: (target, prop) => {
694
+ if (isResolved && realTarget) {
695
+ return Reflect.has(realTarget, prop)
696
+ }
697
+ return Reflect.has(target, prop)
698
+ },
699
+
700
+ ownKeys: (target) => {
701
+ if (isResolved && realTarget) {
702
+ return Reflect.ownKeys(realTarget)
703
+ }
704
+ return Reflect.ownKeys(target)
705
+ },
365
706
 
366
- const attributeResolveOptions = {
707
+ getOwnPropertyDescriptor: (target, prop) => {
708
+ if (isResolved && realTarget) {
709
+ return Reflect.getOwnPropertyDescriptor(realTarget, prop)
710
+ }
711
+ return Reflect.getOwnPropertyDescriptor(target, prop)
712
+ },
713
+
714
+ getPrototypeOf: (target) => {
715
+ if (isResolved && realTarget) {
716
+ return Reflect.getPrototypeOf(realTarget)
717
+ }
718
+ return Reflect.getPrototypeOf(target)
719
+ },
720
+ })
721
+
722
+ return proxy as ResolveProxy<T>
723
+ }
724
+
725
+ /**
726
+ * Internal method to resolve a dependency value with circular dependency handling.
727
+ * This is the core resolution logic that handles caching, proxies, and dependency injection.
728
+ *
729
+ * @param key - The key of the dependency to resolve
730
+ * @param resolveOptions - Options controlling the resolution behavior
731
+ * @returns The resolved dependency value
732
+ */
733
+ protected _resolveValue<K extends ScopeKey<Scope>>(key: K, resolveOptions?: InternalResolveOptions): Scope[K] {
734
+ const attributeResolveOptions: InternalResolveOptions = {
367
735
  ...this.defaultResolveOptions,
368
- ...attribute?.resolveOptions,
736
+ ...this.state[key]?.resolveOptions,
369
737
  ...resolveOptions,
738
+ resolutionStack: resolveOptions?.resolutionStack ?? new Set<PropertyKey>(),
370
739
  }
740
+ const isRoot = attributeResolveOptions.resolutionStack.size === 0
741
+ attributeResolveOptions.resolutionStack.add(key)
742
+ let resolutionContext: Map<PropertyKey, any> = new Map<PropertyKey, any>()
743
+ if (attributeResolveOptions.resolutionContext) {
744
+ // Import existing resolution context if provided, but define a new object to avoid mutating the original
745
+ resolutionContext = new Map(attributeResolveOptions.resolutionContext)
746
+ }
747
+
748
+ // Update the resolve options to use the local copy of the resolution context
749
+ // so that recursive calls don't mutate the original
750
+ attributeResolveOptions.resolutionContext = resolutionContext
751
+
752
+ const attribute = this.state[key]
371
753
 
372
754
  if (!attribute && attributeResolveOptions.strict) {
373
755
  throw new Error(`Dependency ${key?.toString()} not found.`)
@@ -378,10 +760,48 @@ export class Context<Scope extends object = any> {
378
760
  const serveFromCache = attributeResolveOptions.singleton || attributeResolveOptions.eager
379
761
  const dependenciesValid = attribute?.inject?.every((key) => this._cacheIsValid(key as any))
380
762
 
381
- if (serveFromCache && attribute?.cachedValue && dependenciesValid) {
763
+ // Check instance cache for resolution context (including non-singletons)
764
+ const resolutionContextValue = resolutionContext.get(key)
765
+ if (!isRoot && resolutionContextValue) {
766
+ value = resolutionContextValue
767
+ } else if (serveFromCache && attribute?.cachedValue && dependenciesValid) {
382
768
  value = attribute.cachedValue
383
769
  } else {
384
- value = typeof attribute?.value === 'function' ? attribute.value(this) : undefined
770
+ const contextValue = resolutionContext.get(key)
771
+ let proxy: ResolveProxy
772
+ // If the context already has a proxy for this key, use it
773
+ if (isProxy(contextValue)) {
774
+ proxy = contextValue as ResolveProxy
775
+ } else {
776
+ // Create a proxy to use as a placeholder during resolution for circular dependencies
777
+ proxy = this.createProxy()
778
+ }
779
+
780
+ resolutionContext.set(key, proxy)
781
+
782
+ if (contextValue && !isProxy(contextValue)) {
783
+ // If the context has a non-proxy value for this key, use it directly
784
+ value = contextValue
785
+ } else if (proxy.__isResolved) {
786
+ // If the proxy has already been resolved, use its real target
787
+ value = proxy.valueOf() as Scope[K]
788
+ } else {
789
+ // Otherwise, resolve the value normally
790
+ value =
791
+ typeof attribute?.value === 'function' ? attribute.value(this, attributeResolveOptions) : undefined
792
+ }
793
+
794
+ if (value instanceof Promise) {
795
+ const valueAsPromise = value as Scope[K] & Promise<unknown>
796
+ value = valueAsPromise.then((resolvedValue) => {
797
+ proxy.__resolve(resolvedValue)
798
+ resolutionContext.set(key, resolvedValue)
799
+ return isProxy(resolvedValue) ? resolvedValue.valueOf() : resolvedValue
800
+ }) as Scope[K]
801
+ } else {
802
+ proxy.__resolve(value)
803
+ resolutionContext.set(key, value)
804
+ }
385
805
  }
386
806
 
387
807
  if (serveFromCache) {
@@ -398,23 +818,38 @@ export class Context<Scope extends object = any> {
398
818
  /**
399
819
  * Extract a value from context.
400
820
  *
401
- * @param key
402
- * @returns
821
+ * @param key - The scope key to resolve
822
+ * @returns The resolved value or undefined if not found
403
823
  * @deprecated Use `resolve` instead
404
824
  */
405
825
  inject<T = any>(key: ScopeKey<Scope>): T | undefined {
406
826
  return this._resolveValue(key) as T
407
827
  }
408
828
 
829
+ /**
830
+ * Resolves a dependency from the context.
831
+ * This is the primary method for retrieving registered dependencies.
832
+ *
833
+ * @param key - The scope key to resolve
834
+ * @param resolveOptions - Options controlling resolution behavior
835
+ * @returns The resolved dependency value
836
+ */
409
837
  resolve<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K] {
410
- return this._resolveValue(key, resolveOptions)
838
+ // Create a default resolution context for top-level calls if none provided
839
+ const options: InternalResolveOptions = {
840
+ resolutionContext: new Map<PropertyKey, any>(),
841
+ resolutionStack: new Set<PropertyKey>(),
842
+ ...resolveOptions,
843
+ }
844
+ return this._resolveValue(key, options)
411
845
  }
412
846
 
413
847
  /**
414
848
  * Ensure that only one copy of this instance exists in this context. Provides the instance if it doesn't exist yet, otherwise inject the cached instance.
415
849
  *
416
- * @param key
417
- * @param instance
850
+ * @param key - The scope key to register under
851
+ * @param instance - The instance to register as a singleton
852
+ * @returns The singleton instance (either the provided one or the existing one)
418
853
  */
419
854
  singleton<T = any>(key: ScopeKey<Scope>, instance: T) {
420
855
  const existing = this.inject<T>(key)
@@ -427,10 +862,12 @@ export class Context<Scope extends object = any> {
427
862
  }
428
863
 
429
864
  /**
430
- * Instantiate a ContextConsumer class
865
+ * Instantiate a ContextConsumer class.
866
+ * ContextConsumer classes have automatic access to the context instance.
431
867
  *
432
- * @param Consumer
433
- * @returns
868
+ * @param Consumer - The ContextConsumer class to instantiate
869
+ * @param args - Additional arguments to pass to the constructor
870
+ * @returns A new instance of the ContextConsumer
434
871
  */
435
872
  hydrate<T extends ContextConsumer<this, any[]>, A extends any[]>(
436
873
  Consumer: new (context: this, ...args: A) => T,
@@ -440,10 +877,11 @@ export class Context<Scope extends object = any> {
440
877
  }
441
878
 
442
879
  /**
443
- * Create a new context from other instance(s) of Context
880
+ * Create a new context from other instance(s) of Context.
881
+ * Dependencies and event listeners from the provided contexts will be merged into this context.
444
882
  *
445
- * @param contexts
446
- * @returns
883
+ * @param contexts - One or more contexts to extend from
884
+ * @returns The current context instance for chaining
447
885
  */
448
886
  extend(...contexts: Context[]): this {
449
887
  contexts.forEach((context) => {
@@ -460,10 +898,10 @@ export class Context<Scope extends object = any> {
460
898
  }
461
899
 
462
900
  /**
463
- * Modify context with middleware
901
+ * Modify context with middleware.
902
+ * Middleware can register dependencies, modify configuration, or perform other setup tasks.
464
903
  *
465
- * @param middleware
466
- * @returns
904
+ * @param middleware - One or more middleware functions to apply
467
905
  */
468
906
  async use<TNarrowScope extends Partial<Scope>>(
469
907
  ...middleware: ContextMiddleware<Context<TNarrowScope>>[]
@@ -481,17 +919,18 @@ export class Context<Scope extends object = any> {
481
919
  /**
482
920
  * Validate context ensuring all validators are valid.
483
921
  *
484
- * @param validators
485
- * @returns
922
+ * @param validators - One or more validator functions to run
923
+ * @returns The validation result
486
924
  */
487
925
  validate(...validators: Validator<Context>[]) {
488
926
  return validate(this, ...validators)
489
927
  }
490
928
 
491
929
  /**
492
- * Validate context ensuring at least one validator is valid
493
- * @param validators
494
- * @returns
930
+ * Validate context ensuring at least one validator is valid.
931
+ *
932
+ * @param validators - One or more validator functions to run
933
+ * @returns The validation result
495
934
  */
496
935
  validateAny(...validators: Validator<Context>[]) {
497
936
  return validateAny(this, ...validators)
@@ -500,27 +939,26 @@ export class Context<Scope extends object = any> {
500
939
  /**
501
940
  * Add a callback to listen for an event in this context.
502
941
  *
503
- * @param event
504
- * @param listener
505
- * @returns
942
+ * @param type - The event type to listen for
943
+ * @param listener - The callback function to invoke when the event is emitted
944
+ * @returns A function to unregister the listener
506
945
  */
507
946
  on<E extends IEvent = IEvent>(type: IEvent['type'], listener: ContextListener<this, E>) {
508
947
  return this.emitter.on(type, (event) => {
509
- return listener(this, event as E)
948
+ return listener((useContext() ?? this) as this, event as E)
510
949
  })
511
950
  }
512
951
 
513
952
  /**
514
- * Emit an event in this context
953
+ * Emit an event in this context.
515
954
  *
516
- * @param event
517
- * @param args
518
- * @returns
955
+ * @param event - The event type string or event object to emit
956
+ * @returns A promise that resolves when all event listeners have completed
519
957
  */
520
958
  async emit(event: string | IEvent) {
521
959
  const eventObject = typeof event === 'string' ? { type: event } : event
522
960
 
523
- return await this.emitter.emitAsync(eventObject)
961
+ return await withContext(this, () => this.emitter.emitAsync(eventObject))
524
962
  }
525
963
 
526
964
  /**