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
@@ -0,0 +1,60 @@
1
+ import { resolveConstructor } from './target';
2
+
3
+ /** Decorator that writes parameter metadata under `key`; `mapFn` receives the previous value. */
4
+ export const addParamMeta =
5
+ (key: string | symbol, mapFn: (prev: unknown) => unknown): ParameterDecorator =>
6
+ (target, _, parameterIndex) => {
7
+ const metadata: unknown[] = Reflect.getOwnMetadata(key, target) ?? [];
8
+ metadata[parameterIndex] = mapFn(metadata[parameterIndex]);
9
+ Reflect.defineMetadata(key, metadata, target);
10
+ };
11
+ /** Reads parameter metadata written by `addParamMeta`. */
12
+ export const getParamMeta = (key: string | symbol, target: object): unknown[] => {
13
+ return (Reflect.getOwnMetadata(key, resolveConstructor(target)) as unknown[]) ?? [];
14
+ };
15
+
16
+ /** Decorator that attaches a `key` -> `label` pair to a parameter. */
17
+ export const addParamLabel = (key: string, label: string) =>
18
+ addParamMeta('label', (prev: unknown) => {
19
+ const map = (prev as Map<string, string> | undefined) ?? new Map<string, string>();
20
+ return map.set(key, label);
21
+ });
22
+ /** Reads the labels written by `addParamLabel`. */
23
+ export const getParamLabels = (target: object, parameterIndex: number): Map<string, string> => {
24
+ const all = getParamMeta('label', target);
25
+ return (all[parameterIndex] as Map<string, string> | undefined) ?? new Map();
26
+ };
27
+
28
+ /** Decorator that attaches a tag to a parameter. */
29
+ export const addParamTag = (tag: string) =>
30
+ addParamMeta('tag', (prev: unknown) => {
31
+ const set = (prev as Set<string> | undefined) ?? new Set<string>();
32
+ return set.add(tag);
33
+ });
34
+ /** Reads the tags written by `addParamTag`. */
35
+ export const getParamTags = (target: object, parameterIndex: number): Set<string> => {
36
+ const all = getParamMeta('tag', target);
37
+ return (all[parameterIndex] as Set<string> | undefined) ?? new Set();
38
+ };
39
+
40
+ /**
41
+ * Applies several parameter decorators as one, so a stack repeated on many
42
+ * parameters can be given a name:
43
+ *
44
+ * ```typescript
45
+ * const fromConfig = <T>(key: string, map: MapFn<IConfig, T>) =>
46
+ * createComposeParameterDecorator(inject(pipe(by(ConfigToken), map)), addParamLabel('config', key));
47
+ * ```
48
+ *
49
+ * Decorators are applied bottom-up, exactly as stacking them would be, so
50
+ * `@createComposeParameterDecorator(a, b)` behaves like `@a @b`. A parameter
51
+ * decorator returns nothing, so there is nothing to thread - each one is called
52
+ * with the same target, property key and parameter index.
53
+ */
54
+ export const createComposeParameterDecorator =
55
+ (...decorators: ParameterDecorator[]): ParameterDecorator =>
56
+ (target, propertyKey, parameterIndex) => {
57
+ for (let i = decorators.length - 1; i >= 0; i--) {
58
+ decorators[i](target, propertyKey, parameterIndex);
59
+ }
60
+ };
@@ -0,0 +1,25 @@
1
+ import { type constructor, type Instance, Is } from '../utils/basic';
2
+ import { unwrapProxy } from '../utils/ProxyRegistry';
3
+
4
+ /**
5
+ * The class behind `target`: `target` itself when it is already a constructor,
6
+ * otherwise the constructor of the instance.
7
+ *
8
+ * `target` may be a proxy (a `lazy()` provider hands one out), and it is
9
+ * unwrapped before either branch, because metadata is defined on the real class
10
+ * and a proxy is never that class. A proxied class misses the lookup outright,
11
+ * standing in for the very object metadata is keyed by; a proxied instance would
12
+ * only answer `constructor` correctly if its handler forwards the read, which a
13
+ * custom one need not do.
14
+ *
15
+ * The branch tests for a constructor rather than for an instance: `Is.instance`
16
+ * asks for an *own* `constructor` property, which a prototype has and an
17
+ * instance does not - its own `constructor` lives one level up the chain.
18
+ *
19
+ * Every metadata read goes through here, so callers pass whatever they hold - a
20
+ * class, an instance, or a proxy of either - and never unwrap by hand.
21
+ */
22
+ export function resolveConstructor(target: constructor<unknown> | Instance): constructor<unknown> {
23
+ const value = unwrapProxy(target);
24
+ return Is.constructor(value) ? value : (value.constructor as constructor<unknown>);
25
+ }
@@ -0,0 +1,65 @@
1
+ import { IContainer, Tagged } from '../container/IContainer';
2
+ import { InjectOptions, WithScope } from '../injector/IInjector';
3
+ import { Serializable } from '../utils/basic';
4
+
5
+ export type WithLazy = { lazy: boolean };
6
+ /**
7
+ * The resolution context a provider works in: the `scope` resolving, the runtime
8
+ * `args`, and whether the caller asked for a `lazy` instance. One object, so a
9
+ * function handed it destructures what it uses (`({ scope, args }) => ...`).
10
+ */
11
+ export type ProviderOptions = InjectOptions & Partial<WithLazy>;
12
+ /**
13
+ * `ProviderOptions` as a caller which addresses the scope directly passes them -
14
+ * `scope.resolve(key, options)`, `token.resolve(scope, options)` - so the scope is
15
+ * not repeated inside.
16
+ */
17
+ export type ResolveOptions = Omit<ProviderOptions, keyof WithScope>;
18
+ /** A factory: builds the dependency from the resolution context. What `Registration.fromFn` takes. */
19
+ export type ResolveDependency<T = unknown> = (options: ProviderOptions) => T;
20
+ /** What a {@link ScopeAccessRule} sees: the scope asking, the scope holding the provider, and the runtime args. */
21
+ export type ScopeAccessOptions = { invocationScope: Tagged; providerScope: Tagged; args: unknown[] };
22
+ /** Decides whether a provider may be resolved for an invocation; `prev` is the result of earlier rules. Set with `scopeAccess(...)`. */
23
+ export type ScopeAccessRule = (options: ScopeAccessOptions, prev: boolean) => boolean;
24
+
25
+ /** Computes args from the resolution context. */
26
+ export type ArgsFn = (options: InjectOptions) => unknown[];
27
+
28
+ /** Maps runtime args to a `singleton(...)` cache key: one instance per distinct key. */
29
+ export type GetCacheKey = (args: unknown[]) => string | Serializable;
30
+ /** Wraps or replaces a resolved dependency. Used by `decorate(...)`. */
31
+ export type DecorateFn<Instance = any> = (dep: Instance, scope: IContainer) => Instance;
32
+
33
+ /**
34
+ * Provider hooks - the provider's own domain: a dependency was resolved.
35
+ *
36
+ * Resolution is the provider's business, so the hook list belongs to the
37
+ * provider. A dependency is not necessarily a constructed instance - a provider
38
+ * can hand out a value the container never built - so the hook takes `unknown`.
39
+ */
40
+ export type ProviderHook = (dependency: unknown, scope: IContainer) => void;
41
+
42
+ /** The factory behind a registration. See {@link Provider}. */
43
+ export interface IProvider<T = any> {
44
+ resolve(options: ProviderOptions): T;
45
+
46
+ hasAccess(options: ScopeAccessOptions): boolean;
47
+
48
+ map(...mappers: DecorateFn<T>[]): this;
49
+
50
+ addAccessRule(...rules: ScopeAccessRule[]): this;
51
+
52
+ addArgsFn(argsFn: ArgsFn): this;
53
+
54
+ lazy(): this;
55
+
56
+ autoResolve(): this;
57
+
58
+ isAutoResolvable(): boolean;
59
+
60
+ singleton(getCacheKey?: GetCacheKey): this;
61
+
62
+ dispose(): void;
63
+
64
+ onResolved(...hooks: ProviderHook[]): this;
65
+ }
@@ -0,0 +1,174 @@
1
+ import {
2
+ type ArgsFn,
3
+ type DecorateFn,
4
+ type GetCacheKey,
5
+ type IProvider,
6
+ type ProviderOptions,
7
+ type ResolveDependency,
8
+ type ScopeAccessOptions,
9
+ type ScopeAccessRule,
10
+ type ProviderHook,
11
+ } from './IProvider';
12
+ import type { DependencyKey } from '../container/IContainer';
13
+ import { type constructor, toString } from '../utils/basic';
14
+ import { CannotApplySingletonTwiceError } from '../errors/CannotApplySingletonTwiceError';
15
+ import { ProviderDisposedError } from '../errors/ProviderDisposedError';
16
+
17
+ /**
18
+ * The factory behind a registration: creates or returns the dependency and
19
+ * applies the pipes (`singleton`, `lazy`, `decorate`, args and access rules).
20
+ * Usually created for you by {@link Registration}; use it directly only for
21
+ * low-level `container.register(key, provider)` calls.
22
+ */
23
+ export class Provider<T = any> implements IProvider<T> {
24
+ static fromClass<T>(Target: constructor<T>): IProvider<T> {
25
+ return new Provider(({ scope, ...options }) => scope.resolve(Target, options));
26
+ }
27
+
28
+ static fromValue<T>(value: T): IProvider<T> {
29
+ return new Provider(() => value);
30
+ }
31
+
32
+ static fromKey<T>(key: DependencyKey) {
33
+ return new Provider<T>(({ scope }) => scope.resolve(key));
34
+ }
35
+
36
+ private readonly argsFnList: ArgsFn[] = [];
37
+ private readonly accessRules: ScopeAccessRule[] = [];
38
+ private readonly mappers: DecorateFn<T>[] = [];
39
+ private isLazy = false;
40
+ private isAutoResolve = false;
41
+ private cache = new Map<string, unknown>();
42
+ private getKey: GetCacheKey | undefined;
43
+ private isDisposed: boolean = false;
44
+ private readonly onResolvedHookList: ProviderHook[] = [];
45
+
46
+ constructor(private readonly resolveDependency: ResolveDependency<T>) {}
47
+
48
+ /**
49
+ * @throws {ProviderDisposedError} when the provider has already been disposed.
50
+ * @throws {unknown} rethrows whatever an `onResolved` hook threw.
51
+ */
52
+ resolve(options: ProviderOptions): T {
53
+ ProviderDisposedError.assert(
54
+ !this.isDisposed,
55
+ 'Provider is already disposed: its scope was disposed. Resolve from a live scope.',
56
+ );
57
+
58
+ if (!this.getKey) {
59
+ return this.resolveDep(options);
60
+ }
61
+
62
+ const key = toString(this.getKey(options.args ?? []));
63
+
64
+ if (!this.cache.has(key)) {
65
+ this.cache.set(key, this.resolveDep(options));
66
+ }
67
+
68
+ return this.cache.get(key)! as T;
69
+ }
70
+
71
+ /**
72
+ * @throws {unknown} rethrows whatever an `onResolved` hook threw.
73
+ */
74
+ private resolveDep({ scope, args = [], lazy }: ProviderOptions): T {
75
+ let dependency = this.resolveDependency({
76
+ scope,
77
+ args: this.argsFnList.reduce((acc, current) => current({ scope, args: acc }), args),
78
+ lazy: lazy ?? this.isLazy,
79
+ });
80
+ dependency = this.mappers.reduce((acc, current) => current(acc, scope), dependency);
81
+ for (const onResolved of this.onResolvedHookList) {
82
+ onResolved(dependency, scope);
83
+ }
84
+ return dependency;
85
+ }
86
+
87
+ map(...mappers: DecorateFn<T>[]): this {
88
+ this.mappers.push(...mappers);
89
+ return this;
90
+ }
91
+
92
+ addAccessRule(...rules: ScopeAccessRule[]): this {
93
+ this.accessRules.push(...rules);
94
+ return this;
95
+ }
96
+
97
+ lazy(): this {
98
+ this.isLazy = true;
99
+ return this;
100
+ }
101
+
102
+ autoResolve(): this {
103
+ this.isAutoResolve = true;
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * @throws {ProviderDisposedError} when the provider has already been disposed.
109
+ */
110
+ isAutoResolvable(): boolean {
111
+ ProviderDisposedError.assert(
112
+ !this.isDisposed,
113
+ 'Provider is already disposed: its scope was disposed. Resolve from a live scope.',
114
+ );
115
+
116
+ return this.isAutoResolve;
117
+ }
118
+
119
+ addArgsFn(...fns: ArgsFn[]): this {
120
+ this.argsFnList.push(...fns);
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * @throws {ProviderDisposedError} when the provider has already been disposed.
126
+ */
127
+ hasAccess(options: ScopeAccessOptions): boolean {
128
+ ProviderDisposedError.assert(
129
+ !this.isDisposed,
130
+ 'Provider is already disposed: its scope was disposed. Resolve from a live scope.',
131
+ );
132
+
133
+ return this.accessRules.reduce((acc, rule) => rule(options, acc), true);
134
+ }
135
+
136
+ /**
137
+ * @throws {CannotApplySingletonTwiceError} when the provider is already configured as a singleton.
138
+ */
139
+ singleton(getCacheKey: GetCacheKey = () => '1'): this {
140
+ CannotApplySingletonTwiceError.assert(
141
+ !this.getKey,
142
+ 'Provider is already singleton: singleton() was applied twice to the same provider. Remove the duplicate singleton().',
143
+ );
144
+ this.getKey = getCacheKey;
145
+ return this;
146
+ }
147
+
148
+ /**
149
+ * Hooks run after every mapper, on each resolved dependency. A singleton provider
150
+ * caches the dependency, so its hooks run once — on the resolve that filled the cache.
151
+ */
152
+ onResolved(...hooks: ProviderHook[]): this {
153
+ this.onResolvedHookList.push(...hooks);
154
+ return this;
155
+ }
156
+
157
+ /**
158
+ * @throws {ProviderDisposedError} when the provider has already been disposed.
159
+ */
160
+ dispose(): void {
161
+ ProviderDisposedError.assert(
162
+ !this.isDisposed,
163
+ 'Provider is already disposed: its scope was disposed. Resolve from a live scope.',
164
+ );
165
+ this.isDisposed = true;
166
+ this.isAutoResolve = false;
167
+ this.getKey = undefined;
168
+ this.cache.clear();
169
+ this.accessRules.splice(0, this.accessRules.length);
170
+ this.mappers.splice(0, this.mappers.length);
171
+ this.argsFnList.splice(0, this.argsFnList.length);
172
+ this.onResolvedHookList.splice(0, this.onResolvedHookList.length);
173
+ }
174
+ }
@@ -0,0 +1,242 @@
1
+ import { type DependencyKey, type IContainer, type IContainerModule, isDependencyKey } from '../container/IContainer';
2
+ import type { ArgsFn, DecorateFn, GetCacheKey, IProvider, ProviderHook, ScopeAccessRule } from '../provider/IProvider';
3
+ import { SingleToken } from '../token/SingleToken';
4
+ import { BindToken } from '../token/BindToken';
5
+ import { MapFn } from '../utils/fp';
6
+ import { addClassMeta, getClassMeta } from '../metadata/class';
7
+ import { type constructor } from '../utils/basic';
8
+
9
+ /**
10
+ * Decides whether a registration is copied into a scope when that scope is
11
+ * created (or when the registration is added). `prev` is the result of the
12
+ * rules before it, so rules chain. Set with {@link scope} or `registration.when(...)`.
13
+ *
14
+ * @example
15
+ * const onlyRequest: ScopeMatchRule = (s) => s.hasTag('request');
16
+ */
17
+ export type ScopeMatchRule = (s: IContainer, prev: boolean) => boolean;
18
+
19
+ /**
20
+ * A provider transform usable in both places: `registration.pipe(...)` calls
21
+ * `mapProvider`, `@register(...)` calls `mapRegistration`. Build one with
22
+ * {@link registerPipe}; `singleton()`, `lazy()`, `scopeAccess()` etc. are all pipes.
23
+ */
24
+ export interface ProviderPipe<T = unknown> {
25
+ mapProvider(p: IProvider<T>): IProvider<T>;
26
+
27
+ mapRegistration(r: IRegistration<T>): IRegistration<T>;
28
+ }
29
+
30
+ /** Narrows `obj` to a {@link ProviderPipe}. */
31
+ export const isProviderPipe = <T>(obj: unknown): obj is ProviderPipe<T> =>
32
+ obj !== null && typeof obj === 'object' && 'mapProvider' in obj;
33
+
34
+ /**
35
+ * Turns a provider transform into a {@link ProviderPipe}, so it works both in
36
+ * `@register(...)` and in `registration.pipe(...)`.
37
+ *
38
+ * @example
39
+ * const cached = <T>() => registerPipe<T>((p) => p.singleton());
40
+ *
41
+ * @register(bindTo('Cache'), cached())
42
+ * class Cache {}
43
+ */
44
+ export const registerPipe = <T>(mapProvider: (p: IProvider<T>) => IProvider<T>): ProviderPipe<T> => ({
45
+ mapProvider,
46
+ mapRegistration: (r) => r.pipe(mapProvider),
47
+ });
48
+
49
+ /**
50
+ * Anything that can name a binding key: a raw key, or a token that knows how to bind itself.
51
+ */
52
+ export type Bindable<T = any> = DependencyKey | BindToken<T>;
53
+
54
+ /** Normalizes a {@link Bindable}: a raw key becomes a `SingleToken`, a token is returned as is. */
55
+ export const toBindToken = <T>(target: Bindable<T>): BindToken<T> =>
56
+ isDependencyKey(target) ? new SingleToken<T>(target) : target;
57
+
58
+ /**
59
+ * Everything accepted at provider level - by `IRegistration.pipe(...)`.
60
+ */
61
+ export type ProviderMapper<T = any> = MapFn<IProvider<T>> | ProviderPipe<T>;
62
+
63
+ /** Normalizes a {@link ProviderMapper} to a plain provider transform. */
64
+ export const toProviderFn = <T>(mapper: ProviderMapper<T>): MapFn<IProvider<T>> =>
65
+ isProviderPipe<T>(mapper) ? mapper.mapProvider.bind(mapper) : mapper;
66
+
67
+ /**
68
+ * Everything accepted at registration level - by `@register(...)`.
69
+ */
70
+ export type RegistrationMapper<T = any> = MapFn<IRegistration<T>> | ProviderPipe<T> | Bindable<T>;
71
+
72
+ /** Normalizes a {@link RegistrationMapper} to a plain registration transform (a `Bindable` becomes `bindTo(...)`). */
73
+ export const toRegistrationFn = <T>(mapper: RegistrationMapper<T>): MapFn<IRegistration<T>> => {
74
+ if (typeof mapper === 'function') return mapper;
75
+ if (isProviderPipe<T>(mapper)) return (r) => mapper.mapRegistration(r);
76
+ return bindTo(mapper);
77
+ };
78
+
79
+ /**
80
+ * A recipe for putting one provider into matching scopes. Created with
81
+ * `Registration.fromClass` / `fromValue` / `fromFn` / `fromKey` and added with
82
+ * `container.addRegistration(...)`; `applyTo(scope)` registers the provider
83
+ * when the scope rules match.
84
+ */
85
+ export interface IRegistration<T = any> extends IContainerModule {
86
+ getKeyOrFail(): DependencyKey;
87
+
88
+ when(...predicates: ScopeMatchRule[]): this;
89
+
90
+ bindToKey(key: DependencyKey): this;
91
+
92
+ bindTo(key: Bindable): this;
93
+
94
+ pipe(...mappers: ProviderMapper<T>[]): this;
95
+
96
+ bindToAlias(alias: DependencyKey): this;
97
+ }
98
+
99
+ /** The dependency type `T` of an `IRegistration<T>`. */
100
+ export type ReturnTypeOfRegistration<T> = T extends IRegistration<infer R> ? R : never;
101
+
102
+ const METADATA_KEY = 'registration';
103
+ /** Reads the registration transforms written on a class by {@link register}. */
104
+ export const getTransformers = (Target: constructor<unknown>) =>
105
+ getClassMeta<MapFn<IRegistration>[]>(Target, METADATA_KEY) ?? [];
106
+
107
+ /**
108
+ * Class decorator that configures how `Registration.fromClass(Target)` registers
109
+ * the class: binding keys, scope rules and provider pipes. Without a key the
110
+ * class name is used.
111
+ *
112
+ * Pass a key or a token directly - `@register(ILoggerToken)`, `@register('ILogger')`.
113
+ * It is bound exactly as `bindTo(...)` would bind it, so wrapping it in
114
+ * `bindTo(...)` here is redundant; `bindTo` is for the fluent
115
+ * `Registration...` chain.
116
+ *
117
+ * @example
118
+ * @register(ILoggerToken, scope((s) => s.hasTag('application')), singleton())
119
+ * class Logger implements ILogger {}
120
+ *
121
+ * container.addRegistration(Registration.fromClass(Logger));
122
+ */
123
+ export const register = (...mappers: RegistrationMapper[]) =>
124
+ addClassMeta(METADATA_KEY, (acc: MapFn<IRegistration>[] | undefined) => {
125
+ const result = mappers.map((m) => toRegistrationFn(m));
126
+ return acc ? [...result, ...acc] : result;
127
+ });
128
+
129
+ /**
130
+ * Binds a registration to one or more keys or tokens. A `SingleToken` binds its
131
+ * key; an alias token (`toSingleAlias`, `toGroupAlias`) adds an alias.
132
+ *
133
+ * Inside `@register(...)` pass the token itself instead - `@register(ILoggerToken)`
134
+ * binds it the same way, so `@register(bindTo(ILoggerToken))` is redundant.
135
+ *
136
+ * @example
137
+ * Registration.fromClass(Logger).bindTo(ILoggerToken);
138
+ *
139
+ * @example
140
+ * Registration.fromValue(config).bindTo('Config');
141
+ */
142
+ export const bindTo =
143
+ (...tokens: Bindable[]): MapFn<IRegistration> =>
144
+ (r) => {
145
+ for (const token of tokens) {
146
+ toBindToken(token).bindTo(r);
147
+ }
148
+ return r;
149
+ };
150
+
151
+ /**
152
+ * Limits which scopes a registration is copied into. Without it, a registration
153
+ * lands in every scope, so a `singleton()` becomes one instance per scope.
154
+ *
155
+ * @example
156
+ * @register(bindTo('Repo'), scope((s) => s.hasTag('request')))
157
+ * class Repo {}
158
+ */
159
+ export const scope =
160
+ (...rules: ScopeMatchRule[]): MapFn<IRegistration> =>
161
+ (r) =>
162
+ r.when(...rules);
163
+
164
+ /**
165
+ * Appends fixed values after the runtime args handed to the provider.
166
+ *
167
+ * @example
168
+ * @register(bindTo('Client'), appendArgs('https://api.example.com'))
169
+ * class Client {
170
+ * constructor(@inject(arg(0)) readonly baseUrl: string) {}
171
+ * }
172
+ */
173
+ export const appendArgs = <T>(...extraArgs: unknown[]) =>
174
+ registerPipe<T>((p) => p.addArgsFn(({ args = [] }) => [...args, ...extraArgs]));
175
+
176
+ /**
177
+ * Appends values computed at resolve time after the runtime args.
178
+ *
179
+ * @example
180
+ * @register(bindTo('Client'), appendArgsFn(({ scope }) => [scope.resolve('API_URL')]))
181
+ * class Client {}
182
+ */
183
+ export const appendArgsFn = <T>(fn: ArgsFn) =>
184
+ registerPipe<T>((p) => p.addArgsFn((options) => [...(options.args ?? []), ...fn(options)]));
185
+
186
+ /**
187
+ * Controls from which scopes the provider may be resolved (unlike {@link scope},
188
+ * which controls where it is registered). A denied lookup falls through to the parent.
189
+ *
190
+ * @example
191
+ * @register(bindTo('Admin'), scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin')))
192
+ * class AdminService {}
193
+ */
194
+ export const scopeAccess = <T>(rule: ScopeAccessRule) => registerPipe<T>((p) => p.addAccessRule(rule));
195
+
196
+ /**
197
+ * Resolves the dependency as a proxy that constructs the real instance on first
198
+ * member access.
199
+ *
200
+ * @example
201
+ * @register(bindTo('Heavy'), lazy())
202
+ * class HeavyService {}
203
+ */
204
+ export const lazy = <T>() => registerPipe<T>((p) => p.lazy());
205
+
206
+ /**
207
+ * Marks the provider for eager resolution when the scope is set up; needs
208
+ * `container.useModule(new AutoResolveModule())`.
209
+ */
210
+ export const autoResolve = <T>() => registerPipe<T>((p) => p.autoResolve());
211
+
212
+ /**
213
+ * Wraps or replaces the resolved dependency. Order relative to `lazy()` decides
214
+ * whether the proxy or the real instance is decorated.
215
+ *
216
+ * @example
217
+ * @register(bindTo('Api'), decorate((api, scope) => new LoggingApi(api)))
218
+ * class Api {}
219
+ */
220
+ export const decorate = (...fns: DecorateFn[]) => registerPipe((p) => p.map(...fns));
221
+
222
+ /**
223
+ * Caches the dependency per provider, which means per scope the registration
224
+ * was copied into (combine with {@link scope}). `getCacheKey` receives the
225
+ * runtime args and caches one instance per distinct key.
226
+ *
227
+ * @example
228
+ * @register(bindTo('Db'), scope((s) => s.hasTag('application')), singleton())
229
+ * class Db {}
230
+ *
231
+ * @example
232
+ * // one instance per tenant id passed as the first runtime arg
233
+ * @register(bindTo('TenantDb'), singleton(([tenantId]) => String(tenantId)))
234
+ * class TenantDb {}
235
+ */
236
+ export const singleton = <T = unknown>(getCacheKey?: GetCacheKey) => registerPipe<T>((p) => p.singleton(getCacheKey));
237
+
238
+ /**
239
+ * Registration-level form of `IProvider.onResolved`: attaches provider hooks to
240
+ * the piped registration's provider.
241
+ */
242
+ export const onResolve = <T = unknown>(...hooks: ProviderHook[]) => registerPipe<T>((p) => p.onResolved(...hooks));
@@ -0,0 +1,122 @@
1
+ import { DependencyKey, IContainer } from '../container/IContainer';
2
+ import { Provider } from '../provider/Provider';
3
+ import type { IProvider, ResolveDependency } from '../provider/IProvider';
4
+ import { DependencyMissingKeyError } from '../errors/DependencyMissingKeyError';
5
+ import {
6
+ type Bindable,
7
+ type IRegistration,
8
+ type ProviderMapper,
9
+ type ScopeMatchRule,
10
+ getTransformers,
11
+ toBindToken,
12
+ toProviderFn,
13
+ } from './IRegistration';
14
+ import { type MapFn, pipe } from '../utils/fp';
15
+ import { type constructor, Is } from '../utils/basic';
16
+
17
+ /**
18
+ * Builds registrations for `container.addRegistration(...)`. Usually imported as
19
+ * `Registration as R`.
20
+ *
21
+ * - `R.fromClass(Class)` - reads the class's `@register(...)` config; the key
22
+ * defaults to the class name.
23
+ * - `R.fromValue(value).bindTo('Key')` - a constant.
24
+ * - `R.fromFn(({ scope, args }) => ...).bindTo('Key')` - a factory.
25
+ * - `R.fromKey('Other').bindTo('Key')` - an alias of another key.
26
+ *
27
+ * @example
28
+ * container
29
+ * .addRegistration(R.fromClass(Logger))
30
+ * .addRegistration(R.fromValue('https://api.example.com').bindTo('API_URL'))
31
+ * .addRegistration(R.fromFn(({ scope }) => new Client(scope.resolve('API_URL'))).bindTo('Client'));
32
+ */
33
+ export class Registration<T = any> implements IRegistration<T> {
34
+ static fromClass<T>(Target: constructor<T>, { name }: { name?: string } = {}) {
35
+ const transform = pipe(...getTransformers(Target));
36
+ return transform(new Registration(() => Provider.fromClass(Target), name ?? Target.name));
37
+ }
38
+
39
+ static fromValue<T>(value: T) {
40
+ if (Is.constructor(value)) {
41
+ const transform = pipe(...getTransformers(value as constructor<T>));
42
+ return transform(new Registration(() => Provider.fromValue(value), value.name));
43
+ }
44
+ return new Registration(() => Provider.fromValue(value));
45
+ }
46
+
47
+ static fromFn<T>(fn: ResolveDependency<T>) {
48
+ return new Registration(() => new Provider(fn));
49
+ }
50
+
51
+ static fromKey<T>(key: DependencyKey) {
52
+ return new Registration<T>(() => Provider.fromKey(key));
53
+ }
54
+
55
+ private mappers: MapFn<IProvider<T>>[] = [];
56
+ private aliases: Set<DependencyKey> = new Set();
57
+
58
+ constructor(
59
+ private createProvider: () => IProvider<T>,
60
+ public key?: DependencyKey,
61
+ private scopeRules: ScopeMatchRule[] = [],
62
+ ) {}
63
+
64
+ bindToKey(key: DependencyKey): this {
65
+ this.key = key;
66
+ return this;
67
+ }
68
+
69
+ bindToAlias(alias: DependencyKey): this {
70
+ this.aliases.add(alias);
71
+ return this;
72
+ }
73
+
74
+ pipe(...mappers: ProviderMapper<T>[]): this {
75
+ this.mappers.push(...mappers.map(toProviderFn));
76
+ return this;
77
+ }
78
+
79
+ when(...predicates: ScopeMatchRule[]): this {
80
+ this.scopeRules.push(...predicates);
81
+ return this;
82
+ }
83
+
84
+ bindTo(key: Bindable): this {
85
+ toBindToken(key).bindTo(this);
86
+ return this;
87
+ }
88
+
89
+ private matchScope(container: IContainer): boolean {
90
+ return this.scopeRules.reduce((prev, curr) => curr(container, prev), true);
91
+ }
92
+
93
+ /**
94
+ * @throws {DependencyMissingKeyError} when the registration matches the scope but has no binding key.
95
+ */
96
+ applyTo(container: IContainer): void {
97
+ if (!this.matchScope(container)) {
98
+ return;
99
+ }
100
+
101
+ if (!this.key) {
102
+ throw new DependencyMissingKeyError(
103
+ 'No key provided for registration: bind it with @register(bindTo(...)) or .bindTo(...).',
104
+ );
105
+ }
106
+
107
+ const provider = this.mappers.reduce<IProvider<T>>((p, m) => m(p), this.createProvider());
108
+ container.register(this.key, provider, { aliases: [...this.aliases] });
109
+ }
110
+
111
+ /**
112
+ * @throws {DependencyMissingKeyError} when no binding key has been set for this registration.
113
+ */
114
+ getKeyOrFail(): DependencyKey {
115
+ if (!this.key) {
116
+ throw new DependencyMissingKeyError(
117
+ 'No key provided for registration: bind it with @register(bindTo(...)) or .bindTo(...).',
118
+ );
119
+ }
120
+ return this.key;
121
+ }
122
+ }