ts-ioc-container 72.1.0 → 72.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 (255) hide show
  1. package/AGENTS.md +179 -0
  2. package/README.md +169 -0
  3. package/cjm/container/Container.js +5 -2
  4. package/cjm/container/EmptyContainer.js +20 -15
  5. package/cjm/errors/ArgumentNotFoundError.js +1 -0
  6. package/cjm/errors/CannonSingletonApplyTwiceError.js +2 -14
  7. package/cjm/errors/CannotApplySingletonTwiceError.js +18 -0
  8. package/cjm/errors/ContainerDisposedError.js +1 -0
  9. package/cjm/errors/ContainerError.js +1 -0
  10. package/cjm/errors/ContainerNotFoundError.js +1 -0
  11. package/cjm/errors/DependencyMissingKeyError.js +1 -0
  12. package/cjm/errors/DependencyNotFoundError.js +1 -0
  13. package/cjm/errors/MethodNotImplementedError.js +1 -0
  14. package/cjm/errors/ProviderDisposedError.js +1 -0
  15. package/cjm/errors/TypedEventDisposedError.js +1 -0
  16. package/cjm/errors/UnsupportedTokenTypeError.js +1 -0
  17. package/cjm/feature/IFeatureFlags.js +6 -0
  18. package/cjm/index.js +14 -3
  19. package/cjm/provider/Provider.js +6 -6
  20. package/cjm/registration/Registration.js +2 -2
  21. package/cjm/token/BindableToken.js +63 -0
  22. package/cjm/token/ConstantToken.js +4 -4
  23. package/cjm/token/FunctionToken.js +1 -1
  24. package/cjm/token/GroupAliasToken.js +5 -50
  25. package/cjm/token/GroupInstanceToken.js +4 -4
  26. package/cjm/token/MultiVariantFeatureToken.js +84 -0
  27. package/cjm/token/SingleAliasToken.js +5 -50
  28. package/cjm/token/SingleToken.js +5 -50
  29. package/cjm/token/ToggleFeatureToken.js +10 -0
  30. package/cjm/token/toToken.js +1 -1
  31. package/cjm/utils/TypedEvent.js +1 -1
  32. package/cjm/utils/array.js +1 -1
  33. package/esm/container/Container.js +5 -2
  34. package/esm/container/EmptyContainer.js +20 -15
  35. package/esm/errors/ArgumentNotFoundError.js +1 -0
  36. package/esm/errors/CannonSingletonApplyTwiceError.js +2 -13
  37. package/esm/errors/CannotApplySingletonTwiceError.js +14 -0
  38. package/esm/errors/ContainerDisposedError.js +1 -0
  39. package/esm/errors/ContainerError.js +1 -0
  40. package/esm/errors/ContainerNotFoundError.js +1 -0
  41. package/esm/errors/DependencyMissingKeyError.js +1 -0
  42. package/esm/errors/DependencyNotFoundError.js +1 -0
  43. package/esm/errors/MethodNotImplementedError.js +1 -0
  44. package/esm/errors/ProviderDisposedError.js +1 -0
  45. package/esm/errors/TypedEventDisposedError.js +1 -0
  46. package/esm/errors/UnsupportedTokenTypeError.js +1 -0
  47. package/esm/feature/IFeatureFlags.js +3 -0
  48. package/esm/index.js +5 -0
  49. package/esm/provider/Provider.js +6 -6
  50. package/esm/registration/Registration.js +2 -2
  51. package/esm/token/BindableToken.js +59 -0
  52. package/esm/token/ConstantToken.js +4 -4
  53. package/esm/token/FunctionToken.js +1 -1
  54. package/esm/token/GroupAliasToken.js +5 -50
  55. package/esm/token/GroupInstanceToken.js +4 -4
  56. package/esm/token/MultiVariantFeatureToken.js +80 -0
  57. package/esm/token/SingleAliasToken.js +5 -50
  58. package/esm/token/SingleToken.js +5 -50
  59. package/esm/token/ToggleFeatureToken.js +6 -0
  60. package/esm/token/toToken.js +1 -1
  61. package/esm/utils/TypedEvent.js +1 -1
  62. package/esm/utils/array.js +1 -1
  63. package/lib/ExecutionContext.ts +6 -0
  64. package/lib/container/AliasMap.ts +30 -0
  65. package/lib/container/AutoResolveModule.ts +24 -0
  66. package/lib/container/Container.ts +315 -0
  67. package/lib/container/EmptyContainer.ts +170 -0
  68. package/lib/container/IContainer.ts +159 -0
  69. package/lib/errors/ArgumentNotFoundError.ts +13 -0
  70. package/lib/errors/CannonSingletonApplyTwiceError.ts +6 -0
  71. package/lib/errors/CannotApplySingletonTwiceError.ts +22 -0
  72. package/lib/errors/ContainerDisposedError.ts +13 -0
  73. package/lib/errors/ContainerError.ts +15 -0
  74. package/lib/errors/ContainerNotFoundError.ts +13 -0
  75. package/lib/errors/DependencyMissingKeyError.ts +13 -0
  76. package/lib/errors/DependencyNotFoundError.ts +13 -0
  77. package/lib/errors/MethodNotImplementedError.ts +13 -0
  78. package/lib/errors/ProviderDisposedError.ts +22 -0
  79. package/lib/errors/TypedEventDisposedError.ts +22 -0
  80. package/lib/errors/UnsupportedTokenTypeError.ts +13 -0
  81. package/lib/feature/IFeatureFlags.ts +44 -0
  82. package/lib/hooks/HookCollector.ts +109 -0
  83. package/lib/hooks/HookContext.ts +85 -0
  84. package/lib/hooks/combinators.ts +51 -0
  85. package/lib/hooks/hook.ts +89 -0
  86. package/lib/hooks/injectProp.ts +14 -0
  87. package/lib/index.ts +197 -0
  88. package/lib/injector/IInjector.ts +88 -0
  89. package/lib/injector/MetadataInjector.ts +100 -0
  90. package/lib/injector/ProxyInjector.ts +31 -0
  91. package/lib/injector/SimpleInjector.ts +20 -0
  92. package/lib/metadata/class.ts +50 -0
  93. package/lib/metadata/method.ts +47 -0
  94. package/lib/metadata/parameter.ts +60 -0
  95. package/lib/metadata/target.ts +25 -0
  96. package/lib/provider/IProvider.ts +65 -0
  97. package/lib/provider/Provider.ts +174 -0
  98. package/lib/registration/IRegistration.ts +242 -0
  99. package/lib/registration/Registration.ts +122 -0
  100. package/lib/select.ts +33 -0
  101. package/lib/token/BindToken.ts +13 -0
  102. package/lib/token/BindableToken.ts +137 -0
  103. package/lib/token/ClassToken.ts +72 -0
  104. package/lib/token/ConstantToken.ts +50 -0
  105. package/lib/token/FunctionToken.ts +79 -0
  106. package/lib/token/GroupAliasToken.ts +33 -0
  107. package/lib/token/GroupInstanceToken.ts +70 -0
  108. package/lib/token/InjectionToken.ts +46 -0
  109. package/lib/token/MultiVariantFeatureToken.ts +160 -0
  110. package/lib/token/SingleAliasToken.ts +28 -0
  111. package/lib/token/SingleToken.ts +35 -0
  112. package/lib/token/ToggleFeatureToken.ts +27 -0
  113. package/lib/token/toToken.ts +46 -0
  114. package/lib/utils/ProxyRegistry.ts +149 -0
  115. package/lib/utils/TypedEvent.ts +96 -0
  116. package/lib/utils/array.ts +28 -0
  117. package/lib/utils/basic.ts +34 -0
  118. package/lib/utils/debounce.ts +18 -0
  119. package/lib/utils/errorHandler.ts +34 -0
  120. package/lib/utils/fp.ts +86 -0
  121. package/lib/utils/getConstructorChain.ts +17 -0
  122. package/lib/utils/memoize.ts +24 -0
  123. package/lib/utils/once.ts +24 -0
  124. package/lib/utils/shallowCache.ts +22 -0
  125. package/lib/utils/task.ts +33 -0
  126. package/lib/utils/throttle.ts +17 -0
  127. package/package.json +6 -4
  128. package/typings/ExecutionContext.d.ts +2 -0
  129. package/typings/ExecutionContext.d.ts.map +1 -0
  130. package/typings/container/AliasMap.d.ts +1 -0
  131. package/typings/container/AliasMap.d.ts.map +1 -0
  132. package/typings/container/AutoResolveModule.d.ts +16 -0
  133. package/typings/container/AutoResolveModule.d.ts.map +1 -0
  134. package/typings/container/Container.d.ts +61 -0
  135. package/typings/container/Container.d.ts.map +1 -0
  136. package/typings/container/EmptyContainer.d.ts +52 -0
  137. package/typings/container/EmptyContainer.d.ts.map +1 -0
  138. package/typings/container/IContainer.d.ts +77 -0
  139. package/typings/container/IContainer.d.ts.map +1 -0
  140. package/typings/errors/ArgumentNotFoundError.d.ts +3 -0
  141. package/typings/errors/ArgumentNotFoundError.d.ts.map +1 -0
  142. package/typings/errors/CannonSingletonApplyTwiceError.d.ts +6 -6
  143. package/typings/errors/CannonSingletonApplyTwiceError.d.ts.map +1 -0
  144. package/typings/errors/CannotApplySingletonTwiceError.d.ts +12 -0
  145. package/typings/errors/CannotApplySingletonTwiceError.d.ts.map +1 -0
  146. package/typings/errors/ContainerDisposedError.d.ts +3 -0
  147. package/typings/errors/ContainerDisposedError.d.ts.map +1 -0
  148. package/typings/errors/ContainerError.d.ts +7 -0
  149. package/typings/errors/ContainerError.d.ts.map +1 -0
  150. package/typings/errors/ContainerNotFoundError.d.ts +3 -0
  151. package/typings/errors/ContainerNotFoundError.d.ts.map +1 -0
  152. package/typings/errors/DependencyMissingKeyError.d.ts +3 -0
  153. package/typings/errors/DependencyMissingKeyError.d.ts.map +1 -0
  154. package/typings/errors/DependencyNotFoundError.d.ts +3 -0
  155. package/typings/errors/DependencyNotFoundError.d.ts.map +1 -0
  156. package/typings/errors/MethodNotImplementedError.d.ts +3 -0
  157. package/typings/errors/MethodNotImplementedError.d.ts.map +1 -0
  158. package/typings/errors/ProviderDisposedError.d.ts +6 -0
  159. package/typings/errors/ProviderDisposedError.d.ts.map +1 -0
  160. package/typings/errors/TypedEventDisposedError.d.ts +6 -0
  161. package/typings/errors/TypedEventDisposedError.d.ts.map +1 -0
  162. package/typings/errors/UnsupportedTokenTypeError.d.ts +3 -0
  163. package/typings/errors/UnsupportedTokenTypeError.d.ts.map +1 -0
  164. package/typings/feature/IFeatureFlags.d.ts +42 -0
  165. package/typings/feature/IFeatureFlags.d.ts.map +1 -0
  166. package/typings/hooks/HookCollector.d.ts +36 -0
  167. package/typings/hooks/HookCollector.d.ts.map +1 -0
  168. package/typings/hooks/HookContext.d.ts +10 -0
  169. package/typings/hooks/HookContext.d.ts.map +1 -0
  170. package/typings/hooks/combinators.d.ts +23 -0
  171. package/typings/hooks/combinators.d.ts.map +1 -0
  172. package/typings/hooks/hook.d.ts +39 -0
  173. package/typings/hooks/hook.d.ts.map +1 -0
  174. package/typings/hooks/injectProp.d.ts +9 -0
  175. package/typings/hooks/injectProp.d.ts.map +1 -0
  176. package/typings/index.d.ts +6 -0
  177. package/typings/index.d.ts.map +1 -0
  178. package/typings/injector/IInjector.d.ts +32 -0
  179. package/typings/injector/IInjector.d.ts.map +1 -0
  180. package/typings/injector/MetadataInjector.d.ts +58 -0
  181. package/typings/injector/MetadataInjector.d.ts.map +1 -0
  182. package/typings/injector/ProxyInjector.d.ts +13 -0
  183. package/typings/injector/ProxyInjector.d.ts.map +1 -0
  184. package/typings/injector/SimpleInjector.d.ts +13 -0
  185. package/typings/injector/SimpleInjector.d.ts.map +1 -0
  186. package/typings/metadata/class.d.ts +27 -0
  187. package/typings/metadata/class.d.ts.map +1 -0
  188. package/typings/metadata/method.d.ts +24 -0
  189. package/typings/metadata/method.d.ts.map +1 -0
  190. package/typings/metadata/parameter.d.ts +21 -0
  191. package/typings/metadata/parameter.d.ts.map +1 -0
  192. package/typings/metadata/target.d.ts +19 -0
  193. package/typings/metadata/target.d.ts.map +1 -0
  194. package/typings/provider/IProvider.d.ts +25 -0
  195. package/typings/provider/IProvider.d.ts.map +1 -0
  196. package/typings/provider/Provider.d.ts +30 -0
  197. package/typings/provider/Provider.d.ts.map +1 -0
  198. package/typings/registration/IRegistration.d.ts +144 -0
  199. package/typings/registration/IRegistration.d.ts.map +1 -0
  200. package/typings/registration/Registration.d.ts +23 -0
  201. package/typings/registration/Registration.d.ts.map +1 -0
  202. package/typings/select.d.ts +15 -0
  203. package/typings/select.d.ts.map +1 -0
  204. package/typings/token/BindToken.d.ts +3 -0
  205. package/typings/token/BindToken.d.ts.map +1 -0
  206. package/typings/token/BindableToken.d.ts +80 -0
  207. package/typings/token/BindableToken.d.ts.map +1 -0
  208. package/typings/token/ClassToken.d.ts +2 -0
  209. package/typings/token/ClassToken.d.ts.map +1 -0
  210. package/typings/token/ConstantToken.d.ts +14 -0
  211. package/typings/token/ConstantToken.d.ts.map +1 -0
  212. package/typings/token/FunctionToken.d.ts +10 -0
  213. package/typings/token/FunctionToken.d.ts.map +1 -0
  214. package/typings/token/GroupAliasToken.d.ts +21 -22
  215. package/typings/token/GroupAliasToken.d.ts.map +1 -0
  216. package/typings/token/GroupInstanceToken.d.ts +19 -0
  217. package/typings/token/GroupInstanceToken.d.ts.map +1 -0
  218. package/typings/token/InjectionToken.d.ts +8 -0
  219. package/typings/token/InjectionToken.d.ts.map +1 -0
  220. package/typings/token/MultiVariantFeatureToken.d.ts +79 -0
  221. package/typings/token/MultiVariantFeatureToken.d.ts.map +1 -0
  222. package/typings/token/SingleAliasToken.d.ts +16 -22
  223. package/typings/token/SingleAliasToken.d.ts.map +1 -0
  224. package/typings/token/SingleToken.d.ts +25 -21
  225. package/typings/token/SingleToken.d.ts.map +1 -0
  226. package/typings/token/ToggleFeatureToken.d.ts +25 -0
  227. package/typings/token/ToggleFeatureToken.d.ts.map +1 -0
  228. package/typings/token/toToken.d.ts +12 -0
  229. package/typings/token/toToken.d.ts.map +1 -0
  230. package/typings/utils/ProxyRegistry.d.ts +63 -0
  231. package/typings/utils/ProxyRegistry.d.ts.map +1 -0
  232. package/typings/utils/TypedEvent.d.ts +42 -0
  233. package/typings/utils/TypedEvent.d.ts.map +1 -0
  234. package/typings/utils/array.d.ts +7 -0
  235. package/typings/utils/array.d.ts.map +1 -0
  236. package/typings/utils/basic.d.ts +12 -0
  237. package/typings/utils/basic.d.ts.map +1 -0
  238. package/typings/utils/debounce.d.ts +2 -0
  239. package/typings/utils/debounce.d.ts.map +1 -0
  240. package/typings/utils/errorHandler.d.ts +4 -0
  241. package/typings/utils/errorHandler.d.ts.map +1 -0
  242. package/typings/utils/fp.d.ts +8 -0
  243. package/typings/utils/fp.d.ts.map +1 -0
  244. package/typings/utils/getConstructorChain.d.ts +9 -0
  245. package/typings/utils/getConstructorChain.d.ts.map +1 -0
  246. package/typings/utils/memoize.d.ts +10 -0
  247. package/typings/utils/memoize.d.ts.map +1 -0
  248. package/typings/utils/once.d.ts +2 -0
  249. package/typings/utils/once.d.ts.map +1 -0
  250. package/typings/utils/shallowCache.d.ts +2 -0
  251. package/typings/utils/shallowCache.d.ts.map +1 -0
  252. package/typings/utils/task.d.ts +13 -0
  253. package/typings/utils/task.d.ts.map +1 -0
  254. package/typings/utils/throttle.d.ts +2 -0
  255. package/typings/utils/throttle.d.ts.map +1 -0
@@ -1,57 +1,12 @@
1
- import { forwardArgs, InjectionToken } from './InjectionToken.js';
2
- export class SingleToken extends InjectionToken {
3
- token;
4
- _getArgsFn;
5
- _isLazy;
6
- constructor(token, { getArgsFn = forwardArgs, isLazy = false, tags = [], } = {}) {
7
- super(tags);
8
- this.token = token;
9
- this._getArgsFn = getArgsFn;
10
- this._isLazy = isLazy;
11
- }
1
+ import { BindableToken } from './BindableToken.js';
2
+ export class SingleToken extends BindableToken {
12
3
  select(fn) {
13
4
  return (s) => fn(this.resolve(s));
14
5
  }
15
- resolve(s, { args = [], lazy } = {}) {
16
- return s.resolve(this.token, {
17
- args: this._getArgsFn({ scope: s, args }),
18
- lazy: this._isLazy || lazy,
19
- });
6
+ resolve(s, options) {
7
+ return s.resolve(this.token, this.toResolveOptions(s, options));
20
8
  }
21
- bindTo(r) {
9
+ bindKey(r) {
22
10
  r.bindToKey(this.token);
23
11
  }
24
- args(...newArgs) {
25
- const parentFn = this._getArgsFn;
26
- return new SingleToken(this.token, {
27
- getArgsFn: (options) => [...parentFn(options), ...newArgs],
28
- isLazy: this._isLazy,
29
- tags: this.getTags(),
30
- });
31
- }
32
- argsFn(fn) {
33
- const parentFn = this._getArgsFn;
34
- return new SingleToken(this.token, {
35
- getArgsFn: (options) => [...parentFn(options), ...fn(options.scope)],
36
- isLazy: this._isLazy,
37
- tags: this.getTags(),
38
- });
39
- }
40
- lazy() {
41
- return new SingleToken(this.token, {
42
- getArgsFn: this._getArgsFn,
43
- isLazy: true,
44
- tags: this.getTags(),
45
- });
46
- }
47
- addTags(...tags) {
48
- return new SingleToken(this.token, {
49
- getArgsFn: this._getArgsFn,
50
- isLazy: this._isLazy,
51
- tags: [...this.getTags(), ...tags],
52
- });
53
- }
54
- toString() {
55
- return this.token.toString();
56
- }
57
12
  }
@@ -0,0 +1,6 @@
1
+ import { MultiVariantFeatureToken } from './MultiVariantFeatureToken.js';
2
+ export class ToggleFeatureToken extends MultiVariantFeatureToken {
3
+ evaluate(flags, context) {
4
+ return { enabled: flags.isEnabled(this.flag, context) };
5
+ }
6
+ }
@@ -19,6 +19,6 @@ export const toToken = (token) => {
19
19
  if (typeof token === 'function') {
20
20
  return new FunctionToken(token);
21
21
  }
22
- throw new UnsupportedTokenTypeError(`Unknown token ${token}`);
22
+ throw new UnsupportedTokenTypeError(`Unknown token ${token}: expected an InjectionToken, a DependencyKey (string | symbol), a class or a function.`);
23
23
  };
24
24
  export const argToToken = (v) => (isInjectionToken(v) ? v : new ConstantToken(v));
@@ -26,6 +26,6 @@ export class TypedEvent {
26
26
  this.isDisposed = true;
27
27
  }
28
28
  validate() {
29
- TypedEventDisposedError.assert(!this.isDisposed, 'TypedEvent is already disposed');
29
+ TypedEventDisposedError.assert(!this.isDisposed, 'TypedEvent is already disposed: its scope was disposed, so it accepts no new listeners.');
30
30
  }
31
31
  }
@@ -8,7 +8,7 @@ export const Filter = {
8
8
  export const findOrFail = (predicate) => (args = []) => {
9
9
  const index = args.findIndex((arg) => predicate(arg));
10
10
  if (index === -1) {
11
- throw new ArgumentNotFoundError();
11
+ throw new ArgumentNotFoundError(`No argument matches the predicate (received ${args.length} args).`);
12
12
  }
13
13
  return args[index];
14
14
  };
@@ -0,0 +1,6 @@
1
+ import type { IContainer } from './container/IContainer';
2
+
3
+ /** General execution context passed to callbacks that run within a scope. */
4
+ export interface ExecutionContext {
5
+ scope: IContainer;
6
+ }
@@ -0,0 +1,30 @@
1
+ import { type DependencyKey } from './IContainer';
2
+
3
+ export type Alias = DependencyKey;
4
+
5
+ export class AliasMap {
6
+ private readonly aliasToKeySet = new Map<Alias, Set<DependencyKey>>();
7
+
8
+ getKeysByAlias(alias: DependencyKey): DependencyKey[] {
9
+ return [...(this.aliasToKeySet.get(alias) ?? [])];
10
+ }
11
+
12
+ setAliasesByKey(key: DependencyKey, aliases: DependencyKey[]): void {
13
+ // reset existing key's aliases
14
+ for (const [alias, keySet] of this.aliasToKeySet) {
15
+ keySet.delete(key);
16
+ if (keySet.size === 0) {
17
+ this.aliasToKeySet.delete(alias);
18
+ }
19
+ }
20
+
21
+ for (const alias of aliases) {
22
+ const dependencyKeySet = this.aliasToKeySet.get(alias) ?? new Set();
23
+ this.aliasToKeySet.set(alias, dependencyKeySet.add(key));
24
+ }
25
+ }
26
+
27
+ destroy(): void {
28
+ this.aliasToKeySet.clear();
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ import { type AutoResolveOptions, type IContainer, type IContainerModule } from './IContainer';
2
+
3
+ /**
4
+ * Enables eager resolution of providers marked with the `autoResolve()` pipe.
5
+ *
6
+ * Every scope created after this module is applied resolves its own
7
+ * auto-resolvable providers as part of `createScope`, so the instances exist
8
+ * before anyone asks for them. Child scopes inherit the behaviour, so a single
9
+ * `useModule` call on the root covers the whole scope tree.
10
+ *
11
+ * `options.args` are forwarded to every eagerly resolved provider of every such
12
+ * scope. Pass a per-scope value by calling `scope.autoResolve({ args })`
13
+ * directly instead.
14
+ *
15
+ * The container the module is applied to is not a created scope — call
16
+ * `container.autoResolve()` explicitly to eagerly resolve its own providers.
17
+ */
18
+ export class AutoResolveModule implements IContainerModule {
19
+ constructor(private readonly options: AutoResolveOptions = {}) {}
20
+
21
+ applyTo(container: IContainer): void {
22
+ container.scopeCreated.subscribe((scope) => scope.autoResolve(this.options));
23
+ }
24
+ }
@@ -0,0 +1,315 @@
1
+ import {
2
+ type AutoResolveOptions,
3
+ type CreateScopeOptions,
4
+ type DependencyKey,
5
+ type IContainer,
6
+ type IContainerModule,
7
+ type RegisterOptions,
8
+ ResolveManyOptions,
9
+ type ResolveOneOptions,
10
+ type Tag,
11
+ } from './IContainer';
12
+ import { type IInjector } from '../injector/IInjector';
13
+ import { type IProvider } from '../provider/IProvider';
14
+ import { EmptyContainer } from './EmptyContainer';
15
+ import { type IRegistration } from '../registration/IRegistration';
16
+ import { ContainerDisposedError } from '../errors/ContainerDisposedError';
17
+ import { MetadataInjector } from '../injector/MetadataInjector';
18
+ import { AliasMap } from './AliasMap';
19
+ import { unwrapProxy } from '../utils/ProxyRegistry';
20
+ import { DependencyNotFoundError } from '../errors/DependencyNotFoundError';
21
+ import { constructor, Instance, Is } from '../utils/basic';
22
+ import { Filter as F } from '../utils/array';
23
+ import { type ITypedEvent, TypedEvent } from '../utils/TypedEvent';
24
+
25
+ /**
26
+ * A dependency injection scope. Each container is a node in a parent chain:
27
+ * a key not found here (or denied by `scopeAccess`) is resolved from the parent,
28
+ * and the root's parent is an {@link EmptyContainer} that throws
29
+ * `DependencyNotFoundError`. Child scopes come from `createScope({ tags })`,
30
+ * which copies the matching registrations into the child at that moment.
31
+ *
32
+ * @example
33
+ * const app = new Container({ tags: ['application'] })
34
+ * .addRegistration(Registration.fromClass(Logger))
35
+ * .addRegistration(Registration.fromValue(config).bindTo('Config'));
36
+ *
37
+ * const request = app.createScope({ tags: ['request'] });
38
+ * const logger = ILoggerToken.resolve(request);
39
+ * request.dispose();
40
+ */
41
+ export class Container implements IContainer {
42
+ isDisposed = false;
43
+ private parent: IContainer;
44
+ private scopes: IContainer[] = [];
45
+ private readonly instances = new Set<Instance>();
46
+ private registrations: IRegistration[] = [];
47
+ private readonly tags: Set<Tag>;
48
+ private readonly providers = new Map<DependencyKey, IProvider>();
49
+ private readonly aliases = new AliasMap();
50
+ private readonly injector: IInjector;
51
+
52
+ // The concrete events stay private so `emit` is this container's alone; the
53
+ // public properties expose the subscriber's side only.
54
+ private readonly scopeCreatedEvent = new TypedEvent<[IContainer]>();
55
+ private readonly scopeDisposedEvent = new TypedEvent<[IContainer]>();
56
+ private readonly registeredEvent = new TypedEvent<[IProvider, DependencyKey, IContainer]>();
57
+ readonly scopeCreated: ITypedEvent<[IContainer]> = this.scopeCreatedEvent;
58
+ readonly scopeDisposed: ITypedEvent<[IContainer]> = this.scopeDisposedEvent;
59
+ readonly registered: ITypedEvent<[IProvider, DependencyKey, IContainer]> = this.registeredEvent;
60
+
61
+ constructor(
62
+ options: {
63
+ injector?: IInjector;
64
+ parent?: IContainer;
65
+ tags?: Tag[];
66
+ } = {},
67
+ ) {
68
+ this.injector = options.injector ?? new MetadataInjector();
69
+ this.parent = options.parent ?? new EmptyContainer();
70
+ this.tags = new Set(options.tags ?? []);
71
+ }
72
+
73
+ /**
74
+ * @throws {ContainerDisposedError} when the container has already been disposed.
75
+ */
76
+ register(key: DependencyKey, provider: IProvider, { aliases = [] }: RegisterOptions = {}): this {
77
+ this.validateContainer();
78
+ this.providers.set(key, provider);
79
+ this.aliases.setAliasesByKey(key, aliases);
80
+
81
+ // Hooks run once the provider and its aliases are in place, so they observe a resolvable key.
82
+ this.registeredEvent.emit(provider, key, this);
83
+
84
+ return this;
85
+ }
86
+
87
+ /**
88
+ * @throws {ContainerDisposedError} when the container has already been disposed.
89
+ * @throws {DependencyNotFoundError} when `target` cannot be resolved in this container or any parent scope.
90
+ */
91
+ resolve<T>(target: constructor<T> | DependencyKey, { args = [], child = this, lazy }: ResolveOneOptions = {}): T {
92
+ this.validateContainer();
93
+
94
+ if (Is.constructor(target)) {
95
+ return this.injector.resolve(target, { scope: this, args, lazy });
96
+ }
97
+
98
+ const provider = this.providers.get(target) as IProvider<T> | undefined;
99
+
100
+ return provider?.hasAccess({ invocationScope: child, providerScope: this, args })
101
+ ? provider.resolve({ scope: this, args, lazy })
102
+ : this.parent.resolve<T>(target, { args, child, lazy });
103
+ }
104
+
105
+ /**
106
+ * @throws {ContainerDisposedError} when the container has already been disposed.
107
+ * @throws {DependencyNotFoundError} when a key registered under `alias` has no matching provider.
108
+ */
109
+ resolveByAlias<T>(
110
+ alias: DependencyKey,
111
+ { args = [], child = this, lazy, excludedKeys = [] }: ResolveManyOptions = {},
112
+ ): T[] {
113
+ this.validateContainer();
114
+
115
+ const keys: DependencyKey[] = [];
116
+ const deps: T[] = [];
117
+ for (const key of this.aliases.getKeysByAlias(alias).filter(F.exclude(excludedKeys))) {
118
+ const provider = this.findProviderByKeyOrFail<T>(key);
119
+ if (!provider.hasAccess({ invocationScope: child, providerScope: this, args })) {
120
+ continue;
121
+ }
122
+ keys.push(key);
123
+ deps.push(provider.resolve({ scope: this, args, lazy }));
124
+ }
125
+
126
+ const parentDeps = this.parent.resolveByAlias<T>(alias, {
127
+ args,
128
+ child,
129
+ lazy,
130
+ excludedKeys: [...excludedKeys, ...keys],
131
+ });
132
+ return [...deps, ...parentDeps];
133
+ }
134
+
135
+ /**
136
+ * @throws {ContainerDisposedError} when the container has already been disposed.
137
+ * @throws {DependencyNotFoundError} when `alias` cannot be resolved in this container or any parent scope.
138
+ */
139
+ resolveOneByAlias<T>(alias: DependencyKey, { args = [], child = this, lazy }: ResolveOneOptions = {}): T {
140
+ this.validateContainer();
141
+
142
+ const [key] = this.aliases.getKeysByAlias(alias);
143
+ const provider = key ? this.findProviderByKeyOrFail<T>(key) : undefined;
144
+
145
+ return provider?.hasAccess({ invocationScope: child, providerScope: this, args })
146
+ ? provider.resolve({ scope: this, args, lazy })
147
+ : this.parent.resolveOneByAlias<T>(alias, { args, child, lazy });
148
+ }
149
+
150
+ /**
151
+ * @throws {ContainerDisposedError} when the container has already been disposed.
152
+ */
153
+ createScope({ tags }: CreateScopeOptions = {}): IContainer {
154
+ this.validateContainer();
155
+
156
+ // Injector hooks need no copying - the child shares this scope's injector, so it shares its hooks.
157
+ const scope = new Container({ injector: this.injector, parent: this, tags });
158
+ this.scopeCreatedEvent.getListeners().forEach((hook) => scope.scopeCreatedEvent.subscribe(hook));
159
+ this.scopeDisposedEvent.getListeners().forEach((hook) => scope.scopeDisposedEvent.subscribe(hook));
160
+ this.registeredEvent.getListeners().forEach((hook) => scope.registeredEvent.subscribe(hook));
161
+
162
+ for (const registration of this.getRegistrations()) {
163
+ registration.applyTo(scope);
164
+ }
165
+ this.scopes.push(scope);
166
+
167
+ // Hooks run once the scope is fully registered and attached, so they observe a usable scope.
168
+ this.scopeCreatedEvent.emit(scope);
169
+
170
+ return scope;
171
+ }
172
+
173
+ /**
174
+ * Eagerly resolves every provider of this scope which was marked with `autoResolve()`.
175
+ *
176
+ * `args` are forwarded to each of those providers, exactly as `resolve` forwards them.
177
+ *
178
+ * @throws {ContainerDisposedError} when the container has already been disposed.
179
+ */
180
+ autoResolve({ args = [] }: AutoResolveOptions = {}): this {
181
+ this.validateContainer();
182
+
183
+ for (const provider of this.providers.values()) {
184
+ if (provider.isAutoResolvable() && provider.hasAccess({ invocationScope: this, providerScope: this, args })) {
185
+ provider.resolve({ scope: this, args });
186
+ }
187
+ }
188
+
189
+ return this;
190
+ }
191
+
192
+ /**
193
+ * @throws {ContainerDisposedError} when the container has already been disposed.
194
+ */
195
+ dispose(): void {
196
+ this.validateContainer();
197
+ this.isDisposed = true;
198
+
199
+ this.scopeDisposedEvent.emit(this);
200
+
201
+ // Detach from parent
202
+ this.parent.removeScope(this);
203
+ this.parent = new EmptyContainer();
204
+
205
+ // Reset the state
206
+ for (const provider of this.providers.values()) {
207
+ provider.dispose();
208
+ }
209
+ this.providers.clear();
210
+ this.aliases.destroy();
211
+ this.instances.clear();
212
+ this.registrations = [];
213
+
214
+ // Dispose scope events. Injector hooks are not this scope's to clear - the injector outlives it.
215
+ this.scopeCreatedEvent.dispose();
216
+ this.scopeDisposedEvent.dispose();
217
+ this.registeredEvent.dispose();
218
+ }
219
+
220
+ addRegistration(registration: IRegistration): this {
221
+ this.registrations.push(registration);
222
+ registration.applyTo(this);
223
+ return this;
224
+ }
225
+
226
+ getRegistrations(): IRegistration[] {
227
+ return [...this.parent.getRegistrations(), ...this.registrations];
228
+ }
229
+
230
+ getInjector(): IInjector {
231
+ return this.injector;
232
+ }
233
+
234
+ hasRegistration(key: DependencyKey): boolean {
235
+ return this.registrations.some((r) => r.getKeyOrFail() === key) || this.parent.hasRegistration(key);
236
+ }
237
+
238
+ hasAlias(alias: DependencyKey): boolean {
239
+ return this.aliases.getKeysByAlias(alias).length > 0 || this.parent.hasAlias(alias);
240
+ }
241
+
242
+ addInstance(instance: Instance) {
243
+ this.instances.add(instance);
244
+ }
245
+
246
+ getScopes() {
247
+ return [...this.scopes];
248
+ }
249
+
250
+ /**
251
+ * Whether this scope created `instance`.
252
+ *
253
+ * A proxy stands for the object behind it, so it is unwrapped before the
254
+ * lookup - the container tracks real instances, and a caller asking about a
255
+ * proxy is asking about its target. Resolving a lazy proxy's target is what
256
+ * makes that answer possible, so a still-unresolved lazy proxy is resolved
257
+ * here.
258
+ */
259
+ hasInstance(instance: Instance): boolean {
260
+ return this.instances.has(unwrapProxy(instance));
261
+ }
262
+
263
+ removeScope(child: IContainer): void {
264
+ this.scopes = this.scopes.filter((s) => s !== child);
265
+ }
266
+
267
+ useModule(module: IContainerModule): this {
268
+ module.applyTo(this);
269
+ return this;
270
+ }
271
+
272
+ getParent() {
273
+ return this.parent;
274
+ }
275
+
276
+ getInstances(cascade: boolean = false) {
277
+ if (!cascade) {
278
+ return [...this.instances];
279
+ }
280
+ return [...this.instances, ...this.scopes.flatMap((s) => s.getInstances(true))];
281
+ }
282
+
283
+ hasTag(tag: Tag) {
284
+ return this.tags.has(tag);
285
+ }
286
+
287
+ addTags(...tags: Tag[]) {
288
+ for (const tag of tags) {
289
+ this.tags.add(tag);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * @throws {ContainerDisposedError} when the container has already been disposed.
295
+ */
296
+ private validateContainer(): void {
297
+ if (this.isDisposed) {
298
+ throw new ContainerDisposedError(
299
+ 'Container is already disposed: a scope cannot be used after dispose(). Create a new scope.',
300
+ );
301
+ }
302
+ }
303
+
304
+ /**
305
+ * @throws {DependencyNotFoundError} when no provider is registered under `key` in this container.
306
+ */
307
+ private findProviderByKeyOrFail<T>(key: DependencyKey): IProvider<T> {
308
+ if (!this.providers.has(key)) {
309
+ throw new DependencyNotFoundError(
310
+ `Provider ${key.toString()} does not exist: an alias points at a key with no provider in this scope.`,
311
+ );
312
+ }
313
+ return this.providers.get(key)!;
314
+ }
315
+ }
@@ -0,0 +1,170 @@
1
+ import {
2
+ type AutoResolveOptions,
3
+ type DependencyKey,
4
+ type IContainer,
5
+ type IContainerModule,
6
+ type ResolveManyOptions,
7
+ type ResolveOneOptions,
8
+ type Tag,
9
+ } from './IContainer';
10
+ import { MethodNotImplementedError } from '../errors/MethodNotImplementedError';
11
+ import { DependencyNotFoundError } from '../errors/DependencyNotFoundError';
12
+ import { type IProvider } from '../provider/IProvider';
13
+ import { type IRegistration } from '../registration/IRegistration';
14
+ import { type constructor, type Instance } from '../utils/basic';
15
+ import { type IInjector } from '../injector/IInjector';
16
+ import { type ITypedEvent } from '../utils/TypedEvent';
17
+
18
+ /**
19
+ * Terminates the parent chain of every root {@link Container}: `resolve` throws
20
+ * `DependencyNotFoundError`, alias lookups return nothing, and every other
21
+ * method throws `MethodNotImplementedError`. You rarely use it directly.
22
+ */
23
+ export class EmptyContainer implements IContainer {
24
+ /**
25
+ * @throws {MethodNotImplementedError} always — the empty container has no disposal state.
26
+ */
27
+ get isDisposed(): boolean {
28
+ throw new MethodNotImplementedError('EmptyContainer.isDisposed is not implemented');
29
+ }
30
+
31
+ addInstance(instance: Instance) {}
32
+
33
+ getParent() {
34
+ return undefined;
35
+ }
36
+
37
+ getScopes() {
38
+ return [];
39
+ }
40
+
41
+ getInstances() {
42
+ return [];
43
+ }
44
+
45
+ hasInstance(instance: Instance): boolean {
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * @throws {MethodNotImplementedError} always — the empty container cannot create scopes.
51
+ */
52
+ createScope(): IContainer {
53
+ throw new MethodNotImplementedError('EmptyContainer.createScope is not implemented');
54
+ }
55
+
56
+ /**
57
+ * @throws {MethodNotImplementedError} always — the empty container constructs nothing, so it has no injector.
58
+ */
59
+ getInjector(): IInjector {
60
+ throw new MethodNotImplementedError('EmptyContainer.getInjector is not implemented');
61
+ }
62
+
63
+ /**
64
+ * @throws {MethodNotImplementedError} always — the empty container holds no providers to resolve.
65
+ */
66
+ autoResolve(options?: AutoResolveOptions): this {
67
+ throw new MethodNotImplementedError('EmptyContainer.autoResolve is not implemented');
68
+ }
69
+
70
+ /**
71
+ * @throws {MethodNotImplementedError} always — the empty container cannot be disposed.
72
+ */
73
+ dispose(): void {
74
+ throw new MethodNotImplementedError('EmptyContainer.dispose is not implemented');
75
+ }
76
+
77
+ /**
78
+ * @throws {MethodNotImplementedError} always — the empty container cannot hold registrations.
79
+ */
80
+ register(key: DependencyKey, value: IProvider): this {
81
+ throw new MethodNotImplementedError('EmptyContainer.register is not implemented');
82
+ }
83
+
84
+ /**
85
+ * @throws {MethodNotImplementedError} always — the empty container has no tags.
86
+ */
87
+ hasTag(tag: Tag): boolean {
88
+ throw new MethodNotImplementedError('EmptyContainer.hasTag is not implemented');
89
+ }
90
+
91
+ /**
92
+ * @throws {MethodNotImplementedError} always — the empty container has no tags.
93
+ */
94
+ addTags(...tags: Tag[]): void {
95
+ throw new MethodNotImplementedError('EmptyContainer.addTags is not implemented');
96
+ }
97
+
98
+ getRegistrations() {
99
+ return [];
100
+ }
101
+
102
+ hasRegistration(key: DependencyKey): boolean {
103
+ return false;
104
+ }
105
+
106
+ hasAlias(alias: DependencyKey): boolean {
107
+ return false;
108
+ }
109
+
110
+ removeScope(): void {}
111
+
112
+ /**
113
+ * @throws {MethodNotImplementedError} always — the empty container cannot use modules.
114
+ */
115
+ useModule(module: IContainerModule): this {
116
+ throw new MethodNotImplementedError('EmptyContainer.useModule is not implemented');
117
+ }
118
+
119
+ /**
120
+ * @throws {MethodNotImplementedError} always — the empty container cannot hold registrations.
121
+ */
122
+ addRegistration(registration: IRegistration): this {
123
+ throw new MethodNotImplementedError('EmptyContainer.addRegistration is not implemented');
124
+ }
125
+
126
+ /**
127
+ * @throws {DependencyNotFoundError} always — reaching the empty container means `key` was not found in any scope.
128
+ */
129
+ resolve<T>(key: constructor<T> | DependencyKey, options?: ResolveOneOptions): T {
130
+ throw new DependencyNotFoundError(
131
+ `Cannot find ${key.toString()}: no provider for this key in the resolving scope or any parent. ` +
132
+ 'Check that it is registered, that its scope(...) rule matches this scope, that its scopeAccess(...) rule allows it, ' +
133
+ 'and that it was registered before createScope() (existing child scopes are not updated).',
134
+ );
135
+ }
136
+
137
+ resolveByAlias<T>(alias: DependencyKey, options?: ResolveManyOptions): T[] {
138
+ return [];
139
+ }
140
+
141
+ /**
142
+ * @throws {DependencyNotFoundError} always — reaching the empty container means `alias` was not found in any scope.
143
+ */
144
+ resolveOneByAlias<T>(alias: DependencyKey, options?: ResolveOneOptions): T {
145
+ throw new DependencyNotFoundError(
146
+ `Cannot find alias ${alias.toString()}: no accessible registration carries this alias in the resolving scope or any parent.`,
147
+ );
148
+ }
149
+
150
+ /**
151
+ * @throws {MethodNotImplementedError} always — the empty container cannot hold hooks.
152
+ */
153
+ get scopeCreated(): ITypedEvent<[IContainer]> {
154
+ throw new MethodNotImplementedError('EmptyContainer.scopeCreated is not implemented');
155
+ }
156
+
157
+ /**
158
+ * @throws {MethodNotImplementedError} always — the empty container cannot hold hooks.
159
+ */
160
+ get scopeDisposed(): ITypedEvent<[IContainer]> {
161
+ throw new MethodNotImplementedError('EmptyContainer.scopeDisposed is not implemented');
162
+ }
163
+
164
+ /**
165
+ * @throws {MethodNotImplementedError} always — the empty container cannot hold hooks.
166
+ */
167
+ get registered(): ITypedEvent<[IProvider, DependencyKey, IContainer]> {
168
+ throw new MethodNotImplementedError('EmptyContainer.registered is not implemented');
169
+ }
170
+ }