@declaro/core 2.0.0-y.0 → 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 (266) hide show
  1. package/{LICENSE → LICENSE.md} +1 -1
  2. package/README.md +203 -0
  3. package/dist/browser/index.js +28 -0
  4. package/dist/browser/index.js.map +133 -0
  5. package/dist/browser/scope/index.js +3 -0
  6. package/dist/browser/scope/index.js.map +9 -0
  7. package/dist/bun/index.js +19011 -0
  8. package/dist/bun/index.js.map +132 -0
  9. package/dist/bun/scope/index.js +4 -0
  10. package/dist/bun/scope/index.js.map +9 -0
  11. package/dist/node/index.cjs +19039 -0
  12. package/dist/node/index.cjs.map +132 -0
  13. package/dist/node/index.js +19010 -0
  14. package/dist/node/index.js.map +132 -0
  15. package/dist/node/scope/index.cjs +69 -0
  16. package/dist/node/scope/index.cjs.map +9 -0
  17. package/dist/node/scope/index.js +3 -0
  18. package/dist/node/scope/index.js.map +9 -0
  19. package/dist/ts/app/app-context.d.ts +9 -0
  20. package/dist/ts/app/app-context.d.ts.map +1 -0
  21. package/dist/ts/app/app-lifecycle.d.ts +6 -0
  22. package/dist/ts/app/app-lifecycle.d.ts.map +1 -0
  23. package/dist/ts/app/app.d.ts +24 -0
  24. package/dist/ts/app/app.d.ts.map +1 -0
  25. package/dist/{app → ts/app}/index.d.ts +1 -0
  26. package/dist/ts/app/index.d.ts.map +1 -0
  27. package/dist/ts/application/create-request-context.d.ts +4 -0
  28. package/dist/ts/application/create-request-context.d.ts.map +1 -0
  29. package/dist/ts/application/create-request-context.test.d.ts +2 -0
  30. package/dist/ts/application/create-request-context.test.d.ts.map +1 -0
  31. package/dist/ts/application/use-declaro.d.ts +3 -0
  32. package/dist/ts/application/use-declaro.d.ts.map +1 -0
  33. package/dist/{auth → ts/auth}/permission-validator.d.ts +1 -0
  34. package/dist/ts/auth/permission-validator.d.ts.map +1 -0
  35. package/dist/ts/auth/permission-validator.test.d.ts +2 -0
  36. package/dist/ts/auth/permission-validator.test.d.ts.map +1 -0
  37. package/dist/ts/context/async-context.d.ts +54 -0
  38. package/dist/ts/context/async-context.d.ts.map +1 -0
  39. package/dist/ts/context/async-context.test.d.ts +2 -0
  40. package/dist/ts/context/async-context.test.d.ts.map +1 -0
  41. package/dist/{context → ts/context}/context-consumer.d.ts +4 -0
  42. package/dist/ts/context/context-consumer.d.ts.map +1 -0
  43. package/dist/ts/context/context.circular-deps.test.d.ts +2 -0
  44. package/dist/ts/context/context.circular-deps.test.d.ts.map +1 -0
  45. package/dist/ts/context/context.d.ts +452 -0
  46. package/dist/ts/context/context.d.ts.map +1 -0
  47. package/dist/ts/context/context.test.d.ts +2 -0
  48. package/dist/ts/context/context.test.d.ts.map +1 -0
  49. package/dist/ts/context/legacy-context.test.d.ts +2 -0
  50. package/dist/ts/context/legacy-context.test.d.ts.map +1 -0
  51. package/dist/{context → ts/context}/validators.d.ts +2 -1
  52. package/dist/ts/context/validators.d.ts.map +1 -0
  53. package/dist/ts/dataflow/index.d.ts +2 -0
  54. package/dist/ts/dataflow/index.d.ts.map +1 -0
  55. package/dist/ts/dataflow/objects.d.ts +7 -0
  56. package/dist/ts/dataflow/objects.d.ts.map +1 -0
  57. package/dist/ts/dataflow/objects.test.d.ts +2 -0
  58. package/dist/ts/dataflow/objects.test.d.ts.map +1 -0
  59. package/dist/{errors → ts/errors}/errors.d.ts +16 -3
  60. package/dist/ts/errors/errors.d.ts.map +1 -0
  61. package/dist/ts/events/event-manager.d.ts +19 -0
  62. package/dist/ts/events/event-manager.d.ts.map +1 -0
  63. package/dist/ts/events/event-manager.spec.d.ts +2 -0
  64. package/dist/ts/events/event-manager.spec.d.ts.map +1 -0
  65. package/dist/ts/events/index.d.ts +2 -0
  66. package/dist/ts/events/index.d.ts.map +1 -0
  67. package/dist/ts/http/headers.d.ts +21 -0
  68. package/dist/ts/http/headers.d.ts.map +1 -0
  69. package/dist/ts/http/headers.spec.d.ts +2 -0
  70. package/dist/ts/http/headers.spec.d.ts.map +1 -0
  71. package/dist/ts/http/request-context.d.ts +17 -0
  72. package/dist/ts/http/request-context.d.ts.map +1 -0
  73. package/dist/ts/http/request-context.spec.d.ts +2 -0
  74. package/dist/ts/http/request-context.spec.d.ts.map +1 -0
  75. package/dist/ts/http/request.d.ts +31 -0
  76. package/dist/ts/http/request.d.ts.map +1 -0
  77. package/dist/ts/http/request.spec.d.ts +2 -0
  78. package/dist/ts/http/request.spec.d.ts.map +1 -0
  79. package/dist/{http → ts/http}/url.d.ts +5 -4
  80. package/dist/ts/http/url.d.ts.map +1 -0
  81. package/dist/ts/http/url.spec.d.ts +2 -0
  82. package/dist/ts/http/url.spec.d.ts.map +1 -0
  83. package/dist/ts/index.d.ts +47 -0
  84. package/dist/ts/index.d.ts.map +1 -0
  85. package/dist/{pipelines → ts/pipelines}/index.d.ts +1 -0
  86. package/dist/ts/pipelines/index.d.ts.map +1 -0
  87. package/dist/{pipelines → ts/pipelines}/pipeline-action.d.ts +1 -0
  88. package/dist/ts/pipelines/pipeline-action.d.ts.map +1 -0
  89. package/dist/ts/pipelines/pipeline-action.test.d.ts +2 -0
  90. package/dist/ts/pipelines/pipeline-action.test.d.ts.map +1 -0
  91. package/dist/{pipelines → ts/pipelines}/pipeline.d.ts +3 -2
  92. package/dist/ts/pipelines/pipeline.d.ts.map +1 -0
  93. package/dist/ts/pipelines/pipeline.test.d.ts +2 -0
  94. package/dist/ts/pipelines/pipeline.test.d.ts.map +1 -0
  95. package/dist/ts/schema/json-schema.d.ts +12 -0
  96. package/dist/ts/schema/json-schema.d.ts.map +1 -0
  97. package/dist/ts/schema/labels.d.ts +14 -0
  98. package/dist/ts/schema/labels.d.ts.map +1 -0
  99. package/dist/ts/schema/model-schema.d.ts +75 -0
  100. package/dist/ts/schema/model-schema.d.ts.map +1 -0
  101. package/dist/ts/schema/model-schema.test.d.ts +2 -0
  102. package/dist/ts/schema/model-schema.test.d.ts.map +1 -0
  103. package/dist/ts/schema/model.d.ts +35 -0
  104. package/dist/ts/schema/model.d.ts.map +1 -0
  105. package/dist/ts/schema/schema-mixin.d.ts +24 -0
  106. package/dist/ts/schema/schema-mixin.d.ts.map +1 -0
  107. package/dist/ts/schema/test/mock-model.d.ts +8 -0
  108. package/dist/ts/schema/test/mock-model.d.ts.map +1 -0
  109. package/dist/ts/scope/index.d.ts +34 -0
  110. package/dist/ts/scope/index.d.ts.map +1 -0
  111. package/dist/ts/shared/utils/action-descriptor.d.ts +28 -0
  112. package/dist/ts/shared/utils/action-descriptor.d.ts.map +1 -0
  113. package/dist/ts/shared/utils/action-descriptor.test.d.ts +2 -0
  114. package/dist/ts/shared/utils/action-descriptor.test.d.ts.map +1 -0
  115. package/dist/ts/shared/utils/schema-utils.d.ts +3 -0
  116. package/dist/ts/shared/utils/schema-utils.d.ts.map +1 -0
  117. package/dist/ts/shared/utils/schema-utils.test.d.ts +2 -0
  118. package/dist/ts/shared/utils/schema-utils.test.d.ts.map +1 -0
  119. package/dist/ts/shims/async-local-storage.d.ts +36 -0
  120. package/dist/ts/shims/async-local-storage.d.ts.map +1 -0
  121. package/dist/ts/shims/async-local-storage.test.d.ts +2 -0
  122. package/dist/ts/shims/async-local-storage.test.d.ts.map +1 -0
  123. package/dist/{timing.d.ts → ts/timing.d.ts} +1 -0
  124. package/dist/ts/timing.d.ts.map +1 -0
  125. package/dist/{typescript → ts/typescript}/arrays.d.ts +1 -0
  126. package/dist/ts/typescript/arrays.d.ts.map +1 -0
  127. package/dist/{typescript → ts/typescript}/baseModel.d.ts +1 -0
  128. package/dist/ts/typescript/baseModel.d.ts.map +1 -0
  129. package/dist/{typescript → ts/typescript}/classes.d.ts +1 -0
  130. package/dist/ts/typescript/classes.d.ts.map +1 -0
  131. package/dist/{typescript → ts/typescript}/constant-manipulation/snake-case.d.ts +1 -0
  132. package/dist/ts/typescript/constant-manipulation/snake-case.d.ts.map +1 -0
  133. package/dist/{typescript → ts/typescript}/errors.d.ts +1 -0
  134. package/dist/ts/typescript/errors.d.ts.map +1 -0
  135. package/dist/ts/typescript/fetch.d.ts +3 -0
  136. package/dist/ts/typescript/fetch.d.ts.map +1 -0
  137. package/dist/{typescript → ts/typescript}/generics.d.ts +1 -0
  138. package/dist/ts/typescript/generics.d.ts.map +1 -0
  139. package/dist/{typescript → ts/typescript}/index.d.ts +1 -0
  140. package/dist/ts/typescript/index.d.ts.map +1 -0
  141. package/dist/ts/typescript/objects.d.ts +26 -0
  142. package/dist/ts/typescript/objects.d.ts.map +1 -0
  143. package/dist/{typescript → ts/typescript}/promises.d.ts +1 -0
  144. package/dist/ts/typescript/promises.d.ts.map +1 -0
  145. package/dist/{validation → ts/validation}/index.d.ts +1 -0
  146. package/dist/ts/validation/index.d.ts.map +1 -0
  147. package/dist/{validation → ts/validation}/validation.d.ts +1 -0
  148. package/dist/ts/validation/validation.d.ts.map +1 -0
  149. package/dist/{validation → ts/validation}/validator.d.ts +1 -0
  150. package/dist/ts/validation/validator.d.ts.map +1 -0
  151. package/dist/ts/validation/validator.test.d.ts +2 -0
  152. package/dist/ts/validation/validator.test.d.ts.map +1 -0
  153. package/package.json +46 -13
  154. package/src/app/app-context.ts +4 -5
  155. package/src/app/app-lifecycle.ts +4 -3
  156. package/src/app/app.ts +7 -5
  157. package/src/application/create-request-context.test.ts +345 -0
  158. package/src/application/create-request-context.ts +19 -0
  159. package/src/application/use-declaro.ts +27 -0
  160. package/src/auth/permission-validator.test.ts +238 -2
  161. package/src/auth/permission-validator.ts +3 -3
  162. package/src/context/async-context.test.ts +348 -0
  163. package/src/context/async-context.ts +129 -0
  164. package/src/context/context-consumer.ts +4 -4
  165. package/src/context/context.circular-deps.test.ts +1047 -0
  166. package/src/context/context.test.ts +420 -3
  167. package/src/context/context.ts +590 -87
  168. package/src/context/legacy-context.test.ts +9 -9
  169. package/src/dataflow/objects.test.ts +7 -7
  170. package/src/dataflow/objects.ts +10 -9
  171. package/src/errors/errors.ts +19 -3
  172. package/src/events/event-manager.spec.ts +129 -0
  173. package/src/events/event-manager.ts +25 -14
  174. package/src/http/headers.ts +17 -2
  175. package/src/http/request-context.ts +24 -15
  176. package/src/http/request.ts +27 -6
  177. package/src/http/url.ts +3 -3
  178. package/src/index.ts +34 -3
  179. package/src/pipelines/pipeline.test.ts +11 -9
  180. package/src/schema/json-schema.ts +16 -0
  181. package/src/schema/labels.ts +23 -23
  182. package/src/schema/model-schema.test.ts +282 -0
  183. package/src/schema/model-schema.ts +197 -0
  184. package/src/schema/model.ts +143 -0
  185. package/src/schema/schema-mixin.ts +51 -0
  186. package/src/schema/test/mock-model.ts +19 -0
  187. package/src/scope/index.ts +33 -0
  188. package/src/shared/utils/action-descriptor.test.ts +182 -0
  189. package/src/shared/utils/action-descriptor.ts +102 -0
  190. package/src/shared/utils/schema-utils.test.ts +33 -0
  191. package/src/shared/utils/schema-utils.ts +17 -0
  192. package/src/shims/async-local-storage.test.ts +258 -0
  193. package/src/shims/async-local-storage.ts +82 -0
  194. package/src/typescript/objects.ts +32 -1
  195. package/src/validation/validator.test.ts +12 -20
  196. package/dist/app/app-context.d.ts +0 -8
  197. package/dist/app/app-lifecycle.d.ts +0 -4
  198. package/dist/app/app.d.ts +0 -22
  199. package/dist/auth/permission-validator.test.d.ts +0 -1
  200. package/dist/context/context.d.ts +0 -161
  201. package/dist/context/context.test.d.ts +0 -1
  202. package/dist/context/legacy-context.test.d.ts +0 -1
  203. package/dist/dataflow/index.d.ts +0 -1
  204. package/dist/dataflow/objects.d.ts +0 -5
  205. package/dist/dataflow/objects.test.d.ts +0 -1
  206. package/dist/events/event-manager.d.ts +0 -16
  207. package/dist/events/event-manager.spec.d.ts +0 -1
  208. package/dist/events/index.d.ts +0 -1
  209. package/dist/helpers/index.d.ts +0 -1
  210. package/dist/helpers/ucfirst.d.ts +0 -1
  211. package/dist/http/headers.d.ts +0 -4
  212. package/dist/http/headers.spec.d.ts +0 -1
  213. package/dist/http/request-context.d.ts +0 -12
  214. package/dist/http/request-context.spec.d.ts +0 -1
  215. package/dist/http/request.d.ts +0 -8
  216. package/dist/http/request.spec.d.ts +0 -1
  217. package/dist/http/url.spec.d.ts +0 -1
  218. package/dist/index.d.ts +0 -19
  219. package/dist/pipelines/pipeline-action.test.d.ts +0 -1
  220. package/dist/pipelines/pipeline.test.d.ts +0 -1
  221. package/dist/pkg.cjs +0 -30
  222. package/dist/pkg.mjs +0 -56612
  223. package/dist/schema/application.d.ts +0 -83
  224. package/dist/schema/application.test.d.ts +0 -1
  225. package/dist/schema/define-model.d.ts +0 -10
  226. package/dist/schema/define-model.test.d.ts +0 -1
  227. package/dist/schema/formats.d.ts +0 -10
  228. package/dist/schema/index.d.ts +0 -10
  229. package/dist/schema/labels.d.ts +0 -13
  230. package/dist/schema/labels.test.d.ts +0 -1
  231. package/dist/schema/module.d.ts +0 -7
  232. package/dist/schema/module.test.d.ts +0 -1
  233. package/dist/schema/properties.d.ts +0 -19
  234. package/dist/schema/response.d.ts +0 -31
  235. package/dist/schema/response.test.d.ts +0 -1
  236. package/dist/schema/supported-types.d.ts +0 -12
  237. package/dist/schema/supported-types.test.d.ts +0 -1
  238. package/dist/schema/transform-model.d.ts +0 -4
  239. package/dist/schema/transform-model.test.d.ts +0 -1
  240. package/dist/schema/types.d.ts +0 -95
  241. package/dist/schema/types.test.d.ts +0 -1
  242. package/dist/typescript/fetch.d.ts +0 -2
  243. package/dist/typescript/objects.d.ts +0 -12
  244. package/dist/validation/validator.test.d.ts +0 -1
  245. package/src/helpers/index.ts +0 -1
  246. package/src/helpers/ucfirst.ts +0 -3
  247. package/src/schema/application.test.ts +0 -286
  248. package/src/schema/application.ts +0 -150
  249. package/src/schema/define-model.test.ts +0 -81
  250. package/src/schema/define-model.ts +0 -50
  251. package/src/schema/formats.ts +0 -23
  252. package/src/schema/index.ts +0 -10
  253. package/src/schema/labels.test.ts +0 -60
  254. package/src/schema/module.test.ts +0 -39
  255. package/src/schema/module.ts +0 -6
  256. package/src/schema/properties.ts +0 -40
  257. package/src/schema/response.test.ts +0 -101
  258. package/src/schema/response.ts +0 -93
  259. package/src/schema/supported-types.test.ts +0 -20
  260. package/src/schema/supported-types.ts +0 -15
  261. package/src/schema/transform-model.test.ts +0 -31
  262. package/src/schema/transform-model.ts +0 -24
  263. package/src/schema/types.test.ts +0 -28
  264. package/src/schema/types.ts +0 -163
  265. package/tsconfig.json +0 -11
  266. package/vite.config.ts +0 -24
@@ -1,61 +1,245 @@
1
+ import { type IncomingHttpHeaders } from 'http'
1
2
  import { EventManager, type IEvent } from '../events/event-manager'
3
+ import type { Request } from '../http/request'
4
+ import type { AllNodeMiddleware } from '../http/request-context'
2
5
  import type { Class, PromiseOrValue, UnwrapPromise } from '../typescript'
3
6
  import { validate, validateAny, type Validator } from '../validation'
4
7
  import { ContextConsumer } from './context-consumer'
5
- import { cloneDeep } from 'lodash'
8
+ import { useContext, withContext } from './async-context'
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
+ */
14
+ export interface DeclaroDependencies {}
15
+
16
+ /**
17
+ * Base scope interface for contexts with request and node middleware.
18
+ */
19
+ export interface DeclaroScope {
20
+ /** Middleware that runs on request contexts */
21
+ requestMiddleware: ContextMiddleware<Context>[]
22
+ /** Node.js-compatible middleware */
23
+ nodeMiddleware: AllNodeMiddleware[]
24
+ }
6
25
 
7
- export type AppScope = {}
8
- export type RequestScope = {}
9
- export type AppContext = Context<AppScope>
10
- export type RequestContext = Context<RequestScope>
26
+ /**
27
+ * Scope interface for request-specific contexts, extending the base scope with request data.
28
+ */
29
+ export interface DeclaroRequestScope extends DeclaroScope {
30
+ /** The HTTP request object */
31
+ request: Request
32
+ /** Incoming HTTP headers */
33
+ headers: IncomingHttpHeaders
34
+ /** Helper function to retrieve a specific header value */
35
+ header: <K extends keyof IncomingHttpHeaders>(header: K) => IncomingHttpHeaders[K] | undefined
36
+ }
11
37
 
12
- export type ContextMiddleware = (context: Context) => any | Promise<any>
38
+ /**
39
+ * Extracts the scope type from a Context type.
40
+ */
41
+ export type ExtractScope<T extends Context<any>> = T extends Context<infer S> ? S : never
42
+
43
+ /**
44
+ * Creates a context type that narrows the scope to a subset.
45
+ * This allows using a context with more dependencies where fewer are expected.
46
+ */
47
+ export type NarrowContext<TContext extends Context<any>, TNarrowScope extends object> = TContext extends Context<
48
+ infer TFullScope
49
+ >
50
+ ? TNarrowScope extends Partial<TFullScope>
51
+ ? Context<TNarrowScope>
52
+ : never
53
+ : never
54
+
55
+ /**
56
+ * Middleware function that can modify or extend a context.
57
+ */
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
+ */
13
63
  export type ContextState<TContext extends Context> = Record<PropertyKey, ContextAttribute<TContext, StateValue<any>>>
14
64
 
65
+ /**
66
+ * Function that resolves a value from a context.
67
+ */
15
68
  export type ContextResolver<T> = (context: Context) => StateValue<T>
16
69
 
70
+ /**
71
+ * Wrapper type for state values.
72
+ */
17
73
  export type StateValue<T> = T
18
74
 
75
+ /**
76
+ * Types of dependencies that can be registered in a context.
77
+ */
19
78
  export enum DependencyType {
79
+ /** A literal value dependency */
20
80
  VALUE = 'VALUE',
81
+ /** A factory function that creates the dependency */
21
82
  FACTORY = 'FACTORY',
83
+ /** A class constructor that instantiates the dependency */
22
84
  CLASS = 'CLASS',
23
85
  }
24
86
 
87
+ /**
88
+ * Factory function type that takes arguments and returns a value.
89
+ */
25
90
  export type FactoryFn<T, A extends any[]> = (...args: A) => T
26
- 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
+ */
27
100
  export type FilterKeysByType<TScope, TValue> = {
28
101
  [Key in keyof TScope]: TScope[Key] extends TValue ? Key : never
29
102
  }[keyof TScope]
103
+
104
+ /**
105
+ * Filters object keys to only those whose values match a specific type or Promise of that type.
106
+ */
30
107
  export type FilterKeysByAsyncType<TScope, TValue> = {
31
108
  [Key in keyof TScope]: TScope[Key] extends PromiseOrValue<TValue> ? Key : never
32
109
  }[keyof TScope]
110
+
111
+ /**
112
+ * Maps an array of argument types to their corresponding scope keys.
113
+ */
33
114
  export type FilterArgsByType<TScope, TArgs extends any[]> = {
34
115
  [Key in keyof TArgs]: FilterKeysByType<TScope, TArgs[Key]>
35
116
  }
117
+
118
+ /**
119
+ * Maps an array of argument types to their corresponding scope keys for async values.
120
+ */
36
121
  export type FilterAsyncArgsByType<TScope, TArgs extends any[]> = {
37
122
  [Key in keyof TArgs]: FilterKeysByAsyncType<TScope, TArgs[Key]>
38
123
  }
39
124
 
125
+ /**
126
+ * Metadata describing how a dependency is registered and resolved in a context.
127
+ */
40
128
  export type ContextAttribute<TContext extends Context<any>, TValue> = {
129
+ /** The key under which this dependency is registered */
41
130
  key: PropertyKey
131
+ /** Function that loads the value from the context */
42
132
  value?: ValueLoader<TContext, TValue>
133
+ /** The type of dependency (value, factory, or class) */
43
134
  type: DependencyType
44
- resolveOptions: ResolveOptions
135
+ /** Options controlling how this dependency is resolved */
136
+ resolveOptions?: ResolveOptions
137
+ /** Cached value for singleton or eager dependencies */
45
138
  cachedValue?: TValue
139
+ /** Keys of other dependencies that this dependency requires */
46
140
  inject: PropertyKey[]
47
141
  }
48
142
 
143
+ /**
144
+ * Type-safe scope key extraction.
145
+ */
49
146
  export type ScopeKey<S extends object> = keyof S
50
147
 
51
- export type ContextListener = (context: Context) => any
148
+ /**
149
+ * Listener function that responds to events in a context.
150
+ */
151
+ export type ContextListener<C extends Context, E extends IEvent> = (context: C, event: E) => any
152
+
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
+ }
52
191
 
53
- export type ResolveOptions = {
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
+ */
54
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
+ */
55
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
+ */
56
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>
57
215
  }
58
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>
236
+ }
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
+ */
59
243
  export function defaultResolveOptions(): ResolveOptions {
60
244
  return {
61
245
  strict: false,
@@ -64,18 +248,43 @@ export function defaultResolveOptions(): ResolveOptions {
64
248
  }
65
249
  }
66
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
+ */
67
264
  export type ContextOptions = {
265
+ /** Default options to use when resolving dependencies */
68
266
  defaultResolveOptions?: ResolveOptions
69
267
  }
70
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
+ */
71
273
  export class Context<Scope extends object = any> {
72
274
  private readonly state: ContextState<this> = {}
73
275
  private readonly emitter = new EventManager()
74
276
 
277
+ /** The scope object providing typed access to all registered dependencies */
75
278
  public readonly scope: Scope = {} as Scope
76
279
 
280
+ /** Default options used when resolving dependencies if not overridden */
77
281
  protected readonly defaultResolveOptions: ResolveOptions
78
282
 
283
+ /**
284
+ * Creates a new context instance.
285
+ *
286
+ * @param options - Configuration options for the context
287
+ */
79
288
  constructor(options?: ContextOptions) {
80
289
  this.defaultResolveOptions = {
81
290
  ...defaultResolveOptions(),
@@ -83,11 +292,34 @@ export class Context<Scope extends object = any> {
83
292
  }
84
293
  }
85
294
 
295
+ /**
296
+ * Gets the event manager for this context.
297
+ *
298
+ * @returns The event manager instance
299
+ */
300
+ get events() {
301
+ return this.emitter
302
+ }
303
+
304
+ /**
305
+ * Initializes all dependencies marked as eager.
306
+ * Should be called after all dependencies are registered to trigger eager initialization.
307
+ */
308
+ async initializeEagerDependencies() {
309
+ await Promise.all(
310
+ Object.entries(this.state)
311
+ .filter(([, attribute]) => attribute?.resolveOptions?.eager)
312
+ .map(async ([key, attribute]) => {
313
+ await this.resolve(key as any)
314
+ }),
315
+ )
316
+ }
317
+
86
318
  /**
87
319
  * Set a value in context, to be injected later.
88
320
  *
89
- * @param key
90
- * @param payload
321
+ * @param key - The scope key to register the value under
322
+ * @param payload - The value to register
91
323
  * @deprecated Use `provideValue` instead, or you can register the same dependency as a factory with `provideFactory` or class with `provideClass`.
92
324
  */
93
325
  provide<K extends ScopeKey<Scope>>(key: K, payload: Scope[K]) {
@@ -114,19 +346,7 @@ export class Context<Scope extends object = any> {
114
346
  */
115
347
  register<K extends ScopeKey<Scope>>(key: K, dep: ContextAttribute<this, Scope[K]>) {
116
348
  const existingDep = this.state[key]
117
- this.state[key] = dep
118
-
119
- Object.defineProperty(this.scope, key, {
120
- get: () => this.resolve(key),
121
- enumerable: true,
122
- configurable: true,
123
- })
124
-
125
- if (dep?.resolveOptions?.eager) {
126
- this.on('declaro:init', async () => {
127
- await this.resolve(key)
128
- })
129
- }
349
+ this.addDep(key, dep)
130
350
 
131
351
  // kill any cached values that were made by a previous instance of this attribute
132
352
  if (existingDep) {
@@ -138,11 +358,32 @@ export class Context<Scope extends object = any> {
138
358
  }
139
359
  }
140
360
 
361
+ /**
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
367
+ */
368
+ protected addDep<K extends ScopeKey<Scope>>(key: K, dep: ContextAttribute<this, Scope[K]>) {
369
+ this.state[key] = dep
370
+
371
+ Object.defineProperty(this.scope, key, {
372
+ get: () => this.resolve(key),
373
+ enumerable: true,
374
+ configurable: true,
375
+ })
376
+
377
+ return dep
378
+ }
379
+
141
380
  /**
142
381
  * Register a value in context scope.
143
382
  *
144
- * @param key The key to register the dependency under
145
- * @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
146
387
  */
147
388
  registerValue<K extends ScopeKey<Scope>>(key: K, value: Scope[K], defaultResolveOptions?: ResolveOptions) {
148
389
  const attribute: ContextAttribute<this, Scope[K]> = {
@@ -161,9 +402,10 @@ export class Context<Scope extends object = any> {
161
402
  /**
162
403
  * Register a dependency as a factory in context scope.
163
404
  *
164
- * @param key The key to register the dependency under
165
- * @param factory A factory function that will be called to generate the value when it is requested.
166
- * @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
167
409
  * @returns A chainable instance of context
168
410
  */
169
411
  registerFactory<K extends ScopeKey<Scope>, A extends any[]>(
@@ -173,8 +415,10 @@ export class Context<Scope extends object = any> {
173
415
  defaultResolveOptions?: ResolveOptions,
174
416
  ) {
175
417
  const attribute: ContextAttribute<this, Scope[K]> = {
176
- value: (context) => {
177
- 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
178
422
 
179
423
  return factory(...args)
180
424
  },
@@ -189,6 +433,16 @@ export class Context<Scope extends object = any> {
189
433
  return this
190
434
  }
191
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
+ */
192
446
  registerAsyncFactory<K extends FilterKeysByType<Scope, Promise<any>>, A extends any[]>(
193
447
  key: K,
194
448
  factory: FactoryFn<Scope[K], A>,
@@ -196,8 +450,10 @@ export class Context<Scope extends object = any> {
196
450
  defaultResolveOptions?: ResolveOptions,
197
451
  ) {
198
452
  const attribute: ContextAttribute<this, Scope[K]> = {
199
- value: (async (context) => {
200
- 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
201
457
 
202
458
  return await factory(...args)
203
459
  }) as ValueLoader<this, Scope[K]>,
@@ -212,15 +468,28 @@ export class Context<Scope extends object = any> {
212
468
  return this
213
469
  }
214
470
 
215
- registerClass<K extends FilterKeysByType<Scope, InstanceType<T>>, T extends Class<Scope[K]>>(
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
+ */
481
+ registerClass<
482
+ K extends FilterKeysByType<Scope, InstanceType<T>>,
483
+ T extends Class<Scope[K] extends {} ? Scope[K] : never>,
484
+ >(
216
485
  key: K,
217
486
  Class: T,
218
487
  inject?: FilterArgsByType<Scope, ConstructorParameters<T>>,
219
488
  defaultResolveOptions?: ResolveOptions,
220
489
  ) {
221
490
  const attribute: ContextAttribute<this, Scope[K]> = {
222
- value: (context) => {
223
- const args = inject?.map((key) => context.resolve(key)) ?? []
491
+ value: (context, resolveOptions) => {
492
+ const args = inject?.map((key) => context.resolve(key, getNestedResolveOptions(resolveOptions))) ?? []
224
493
 
225
494
  return new (Class as any)(...(args as any))
226
495
  },
@@ -235,16 +504,30 @@ export class Context<Scope extends object = any> {
235
504
  return this
236
505
  }
237
506
 
238
- registerAsyncClass<K extends FilterKeysByType<Scope, InstanceType<any>>, T extends Class<UnwrapPromise<Scope[K]>>>(
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
+ */
517
+ registerAsyncClass<
518
+ K extends FilterKeysByType<Scope, InstanceType<any>>,
519
+ T extends Class<UnwrapPromise<Scope[K]> extends {} ? UnwrapPromise<Scope[K]> : never>,
520
+ >(
239
521
  key: K,
240
522
  Class: T,
241
523
  inject?: FilterAsyncArgsByType<Scope, ConstructorParameters<T>>,
242
524
  defaultResolveOptions?: ResolveOptions,
243
525
  ) {
244
526
  const attribute: ContextAttribute<this, Scope[K]> = {
245
- value: (async (context) => {
527
+ value: (async (context, resolveOptions) => {
246
528
  const args = (await Promise.all(
247
- (inject?.map((key) => context.resolve(key)) ?? []) as ConstructorParameters<T>,
529
+ (inject?.map((key) => context.resolve(key, getNestedResolveOptions(resolveOptions))) ??
530
+ []) as ConstructorParameters<T>,
248
531
  )) as ConstructorParameters<T>
249
532
 
250
533
  return new (Class as any)(...(args as any))
@@ -260,6 +543,12 @@ export class Context<Scope extends object = any> {
260
543
  return this
261
544
  }
262
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
+ */
263
552
  getAllDependencies<K extends ScopeKey<Scope>>(key: K): ContextAttribute<this, any>[] {
264
553
  const attribute = this.state[key]
265
554
 
@@ -277,52 +566,191 @@ export class Context<Scope extends object = any> {
277
566
  return dependencies
278
567
  }
279
568
 
280
- 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
+
281
584
  const dependents = Object.entries(this.state)
282
585
  .filter(([_, attribute]) => attribute.inject?.includes(key))
283
586
  .map(([key, attribute]) => attribute)
284
587
 
285
588
  dependents.forEach((dependent) => {
286
- const nestedDependents = this.getAllDependents(dependent.key as any)
589
+ const nestedDependents = this.getAllDependents(dependent.key as any, visited)
287
590
  dependents.push(...nestedDependents)
288
591
  })
289
592
 
290
593
  return dependents
291
594
  }
292
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
+ */
293
602
  introspect<K extends ScopeKey<Scope>>(key: K) {
294
603
  const attribute = this.state[key]
295
604
 
296
605
  return attribute
297
606
  }
298
607
 
299
- 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
+
300
622
  const attribute = this.state[key]
301
623
 
302
- const needsCache = attribute.resolveOptions?.singleton || attribute.resolveOptions?.eager
624
+ const needsCache = attribute?.resolveOptions?.singleton || attribute?.resolveOptions?.eager
303
625
 
304
626
  if (!needsCache) {
305
627
  return true
306
628
  }
307
629
 
308
- const hasCachedValue = attribute.cachedValue !== undefined && attribute.cachedValue !== null
630
+ const hasCachedValue = attribute?.cachedValue !== undefined && attribute?.cachedValue !== null
309
631
 
310
632
  if (!hasCachedValue) {
311
633
  return false
312
634
  }
313
635
 
314
- 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
315
641
  }
316
642
 
317
- protected _resolveValue<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K] {
318
- 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
+ },
706
+
707
+ getOwnPropertyDescriptor: (target, prop) => {
708
+ if (isResolved && realTarget) {
709
+ return Reflect.getOwnPropertyDescriptor(realTarget, prop)
710
+ }
711
+ return Reflect.getOwnPropertyDescriptor(target, prop)
712
+ },
319
713
 
320
- const attributeResolveOptions = {
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 = {
321
735
  ...this.defaultResolveOptions,
322
- ...attribute?.resolveOptions,
736
+ ...this.state[key]?.resolveOptions,
323
737
  ...resolveOptions,
738
+ resolutionStack: resolveOptions?.resolutionStack ?? new Set<PropertyKey>(),
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)
324
746
  }
325
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]
753
+
326
754
  if (!attribute && attributeResolveOptions.strict) {
327
755
  throw new Error(`Dependency ${key?.toString()} not found.`)
328
756
  }
@@ -332,10 +760,48 @@ export class Context<Scope extends object = any> {
332
760
  const serveFromCache = attributeResolveOptions.singleton || attributeResolveOptions.eager
333
761
  const dependenciesValid = attribute?.inject?.every((key) => this._cacheIsValid(key as any))
334
762
 
335
- 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) {
336
768
  value = attribute.cachedValue
337
769
  } else {
338
- value = attribute?.value(this)
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
+ }
339
805
  }
340
806
 
341
807
  if (serveFromCache) {
@@ -352,23 +818,38 @@ export class Context<Scope extends object = any> {
352
818
  /**
353
819
  * Extract a value from context.
354
820
  *
355
- * @param key
356
- * @returns
821
+ * @param key - The scope key to resolve
822
+ * @returns The resolved value or undefined if not found
357
823
  * @deprecated Use `resolve` instead
358
824
  */
359
825
  inject<T = any>(key: ScopeKey<Scope>): T | undefined {
360
826
  return this._resolveValue(key) as T
361
827
  }
362
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
+ */
363
837
  resolve<K extends ScopeKey<Scope>>(key: K, resolveOptions?: ResolveOptions): Scope[K] {
364
- 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)
365
845
  }
366
846
 
367
847
  /**
368
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.
369
849
  *
370
- * @param key
371
- * @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)
372
853
  */
373
854
  singleton<T = any>(key: ScopeKey<Scope>, instance: T) {
374
855
  const existing = this.inject<T>(key)
@@ -381,10 +862,12 @@ export class Context<Scope extends object = any> {
381
862
  }
382
863
 
383
864
  /**
384
- * Instantiate a ContextConsumer class
865
+ * Instantiate a ContextConsumer class.
866
+ * ContextConsumer classes have automatic access to the context instance.
385
867
  *
386
- * @param Consumer
387
- * @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
388
871
  */
389
872
  hydrate<T extends ContextConsumer<this, any[]>, A extends any[]>(
390
873
  Consumer: new (context: this, ...args: A) => T,
@@ -394,49 +877,60 @@ export class Context<Scope extends object = any> {
394
877
  }
395
878
 
396
879
  /**
397
- * 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.
398
882
  *
399
- * @param contexts
400
- * @returns
883
+ * @param contexts - One or more contexts to extend from
884
+ * @returns The current context instance for chaining
401
885
  */
402
886
  extend(...contexts: Context[]): this {
403
887
  contexts.forEach((context) => {
404
888
  Reflect.ownKeys(context.state).forEach((key) => {
405
- const dep = cloneDeep(context.state[key])
406
- this.register(key as any, dep)
889
+ // const dep = cloneDeep(context.state[key])
890
+ const dep = { ...context.state[key] }
891
+ this.addDep(key as any, dep)
407
892
  })
893
+
894
+ this.emitter.extend(context.emitter)
408
895
  })
409
896
 
410
897
  return this
411
898
  }
412
899
 
413
900
  /**
414
- * Modify context with middleware
901
+ * Modify context with middleware.
902
+ * Middleware can register dependencies, modify configuration, or perform other setup tasks.
415
903
  *
416
- * @param middleware
417
- * @returns
904
+ * @param middleware - One or more middleware functions to apply
418
905
  */
906
+ async use<TNarrowScope extends Partial<Scope>>(
907
+ ...middleware: ContextMiddleware<Context<TNarrowScope>>[]
908
+ ): Promise<void>
909
+ async use(...middleware: ContextMiddleware[]): Promise<void>
419
910
  async use(...middleware: ContextMiddleware[]) {
420
911
  return middleware.reduce(async (promise, middleware) => {
421
912
  await promise
422
913
  await middleware(this)
914
+
915
+ return undefined
423
916
  }, Promise.resolve(undefined))
424
917
  }
425
918
 
426
919
  /**
427
920
  * Validate context ensuring all validators are valid.
428
921
  *
429
- * @param validators
430
- * @returns
922
+ * @param validators - One or more validator functions to run
923
+ * @returns The validation result
431
924
  */
432
925
  validate(...validators: Validator<Context>[]) {
433
926
  return validate(this, ...validators)
434
927
  }
435
928
 
436
929
  /**
437
- * Validate context ensuring at least one validator is valid
438
- * @param validators
439
- * @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
440
934
  */
441
935
  validateAny(...validators: Validator<Context>[]) {
442
936
  return validateAny(this, ...validators)
@@ -445,26 +939,35 @@ export class Context<Scope extends object = any> {
445
939
  /**
446
940
  * Add a callback to listen for an event in this context.
447
941
  *
448
- * @param event
449
- * @param listener
450
- * @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
451
945
  */
452
- on(event: string, listener: ContextListener) {
453
- return this.emitter.on(event, (_) => {
454
- return listener(this)
946
+ on<E extends IEvent = IEvent>(type: IEvent['type'], listener: ContextListener<this, E>) {
947
+ return this.emitter.on(type, (event) => {
948
+ return listener((useContext() ?? this) as this, event as E)
455
949
  })
456
950
  }
457
951
 
458
952
  /**
459
- * Emit an event in this context
953
+ * Emit an event in this context.
460
954
  *
461
- * @param event
462
- * @param args
463
- * @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
464
957
  */
465
- async emit(event: string) {
466
- return await this.emitter.emitAsync({
467
- type: event,
468
- })
958
+ async emit(event: string | IEvent) {
959
+ const eventObject = typeof event === 'string' ? { type: event } : event
960
+
961
+ return await withContext(this, () => this.emitter.emitAsync(eventObject))
962
+ }
963
+
964
+ /**
965
+ * Narrow the context to a subset of its scope type.
966
+ * This is a type-only operation - it returns the same instance with a narrower type.
967
+ *
968
+ * @returns A context with a narrower scope type that shares the same underlying state
969
+ */
970
+ narrow<TNarrowScope extends Partial<Scope>>(): NarrowContext<this, TNarrowScope> {
971
+ return this as unknown as NarrowContext<this, TNarrowScope>
469
972
  }
470
973
  }