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,160 @@
1
+ import { type DependencyKey, type IContainer } from '../container/IContainer';
2
+ import { forwardArgs, InjectionToken } from './InjectionToken';
3
+ import { type ArgsFn, type ResolveOptions } from '../provider/IProvider';
4
+ import { type Serializable } from '../utils/basic';
5
+ import { ProxyRegistry } from '../utils/ProxyRegistry';
6
+ import { DependencyNotFoundError } from '../errors/DependencyNotFoundError';
7
+ import {
8
+ type FeatureContext,
9
+ type IFeatureFlags,
10
+ IFeatureContextToken,
11
+ IFeatureFlagsToken,
12
+ } from '../feature/IFeatureFlags';
13
+
14
+ export type FeatureTokenOptions = { getArgsFn?: ArgsFn; isLazy?: boolean; tags?: string[] };
15
+
16
+ /** What a feature token reads from its flag: whether it is on, and the named variant served, if any. */
17
+ export type FeatureEvaluation<V extends string = string> = { enabled: boolean; variant?: V };
18
+
19
+ /**
20
+ * A dependency switched by a multi-variant feature flag. The implementations
21
+ * decide where they belong - a bindable token declares itself
22
+ *
23
+ * - a named variant with `variantOf(feature, name)`: used while the flag serves `name`;
24
+ * - the primary variant with `primaryVariantOf(feature)`: used while the flag is
25
+ * on and no named variant implementation matches;
26
+ * - the fallback with `fallbackOf(feature)`: used otherwise - the flag is off,
27
+ * or its client throws.
28
+ *
29
+ * A flag always has a fallback: resolving a feature without one registered fails.
30
+ *
31
+ * `flag` is the flag's name on the flag service. `V` narrows the variant names
32
+ * implementations may claim. The flag client comes from `IFeatureFlagsToken`
33
+ * and the context from `IFeatureContextToken`, both resolved from the resolving
34
+ * scope, on every resolution.
35
+ *
36
+ * Subclasses change only how the served variant is evaluated - see
37
+ * `ToggleFeatureToken`, the on/off case.
38
+ *
39
+ * @example
40
+ * const CheckoutButtonToken = new MultiVariantFeatureToken<IButton, 'blue' | 'green'>('checkout-button');
41
+ *
42
+ * @register(new SingleToken<IButton>('GreyButton').fallbackOf(CheckoutButtonToken))
43
+ * class GreyButton implements IButton {}
44
+ *
45
+ * @register(new SingleToken<IButton>('BlueButton').variantOf(CheckoutButtonToken, 'blue'))
46
+ * class BlueButton implements IButton {}
47
+ *
48
+ * @register(new SingleToken<IButton>('GreenButton').primaryVariantOf(CheckoutButtonToken))
49
+ * class GreenButton implements IButton {}
50
+ */
51
+ export class MultiVariantFeatureToken<T = any, V extends string = string>
52
+ extends InjectionToken<T>
53
+ implements Serializable
54
+ {
55
+ private readonly _getArgsFn: ArgsFn;
56
+ private readonly _isLazy: boolean;
57
+
58
+ constructor(
59
+ readonly flag: string,
60
+ { getArgsFn = forwardArgs, isLazy = false, tags = [] }: FeatureTokenOptions = {},
61
+ ) {
62
+ super(tags);
63
+ this._getArgsFn = getArgsFn;
64
+ this._isLazy = isLazy;
65
+ }
66
+
67
+ /** The alias an implementation of the variant `name` is bound to. Declare it with `token.variantOf(feature, name)`. */
68
+ variantAlias(name: V): DependencyKey {
69
+ return `feature:${this.flag}:variant:${name}`;
70
+ }
71
+
72
+ /** The alias the primary variant is bound to. Declare it with `token.primaryVariantOf(feature)`. */
73
+ primaryAlias(): DependencyKey {
74
+ return `feature:${this.flag}:primary`;
75
+ }
76
+
77
+ /** The alias the fallback is bound to. Declare it with `token.fallbackOf(feature)`. */
78
+ fallbackAlias(): DependencyKey {
79
+ return `feature:${this.flag}:fallback`;
80
+ }
81
+
82
+ /**
83
+ * @throws {DependencyNotFoundError} when the flag has no fallback, or the flag client, the feature context or the selected implementation cannot be resolved.
84
+ * @throws {ContainerDisposedError} when `s` has already been disposed.
85
+ */
86
+ resolve(s: IContainer, { args = [], lazy }: ResolveOptions = {}): T {
87
+ const resolveSelected = () =>
88
+ s.resolveOneByAlias<T>(this.selectAlias(s), { args: this._getArgsFn({ scope: s, args }) });
89
+ return ProxyRegistry.getInstance().toLazyIf(resolveSelected as () => T & object, this._isLazy || lazy);
90
+ }
91
+
92
+ args(...newArgs: unknown[]): this {
93
+ const parentFn = this._getArgsFn;
94
+ return this.copy({ getArgsFn: (options) => [...parentFn(options), ...newArgs] });
95
+ }
96
+
97
+ argsFn(fn: (s: IContainer) => unknown[]): this {
98
+ const parentFn = this._getArgsFn;
99
+ return this.copy({ getArgsFn: (options) => [...parentFn(options), ...fn(options.scope)] });
100
+ }
101
+
102
+ lazy(): this {
103
+ return this.copy({ isLazy: true });
104
+ }
105
+
106
+ addTags(...tags: string[]): this {
107
+ return this.copy({ tags: [...this.getTags(), ...tags] });
108
+ }
109
+
110
+ toString(): string {
111
+ return this.flag;
112
+ }
113
+
114
+ /** Whether the flag is on for `context`, and the named variant it serves, if any. */
115
+ protected evaluate(flags: IFeatureFlags, context: FeatureContext): FeatureEvaluation<V> {
116
+ const served = flags.getVariant(this.flag, context);
117
+ return served.enabled
118
+ ? { enabled: true, variant: served.name as V }
119
+ : { enabled: flags.isEnabled(this.flag, context) };
120
+ }
121
+
122
+ private copy(overrides: FeatureTokenOptions): this {
123
+ const Self = this.constructor as new (flag: string, options: FeatureTokenOptions) => this;
124
+ return new Self(this.flag, {
125
+ getArgsFn: this._getArgsFn,
126
+ isLazy: this._isLazy,
127
+ tags: this.getTags(),
128
+ ...overrides,
129
+ });
130
+ }
131
+
132
+ /**
133
+ * The alias of the served variant's implementation, or of the fallback.
134
+ *
135
+ * @throws {DependencyNotFoundError} when the flag has no fallback, or the flag client or the feature context cannot be resolved.
136
+ */
137
+ private selectAlias(s: IContainer): DependencyKey {
138
+ const fallback = this.fallbackAlias();
139
+ if (!s.hasAlias(fallback)) {
140
+ throw new DependencyNotFoundError(
141
+ `Feature flag "${this.flag}" has no fallback: a flag always has one. ` +
142
+ `Register it with @register(token.fallbackOf(feature)) in this scope or a parent.`,
143
+ );
144
+ }
145
+
146
+ const flags = IFeatureFlagsToken.resolve(s);
147
+ const context = IFeatureContextToken.resolve(s);
148
+ let served: FeatureEvaluation<V> = { enabled: false };
149
+ try {
150
+ served = this.evaluate(flags, context);
151
+ } catch {
152
+ // A flag always has a fallback: a failing client serves it.
153
+ }
154
+ const candidates = [
155
+ served.variant === undefined ? undefined : this.variantAlias(served.variant),
156
+ served.enabled ? this.primaryAlias() : undefined,
157
+ ];
158
+ return candidates.find((alias) => alias !== undefined && s.hasAlias(alias)) ?? fallback;
159
+ }
160
+ }
@@ -0,0 +1,28 @@
1
+ import { type DependencyKey, type IContainer } from '../container/IContainer';
2
+ import { BindableToken } from './BindableToken';
3
+ import { type IRegistration } from '../registration/IRegistration';
4
+ import { type ResolveOptions } from '../provider/IProvider';
5
+
6
+ /**
7
+ * Like {@link GroupAliasToken}, but resolves exactly one dependency registered
8
+ * under the alias, and throws `DependencyNotFoundError` when there is none.
9
+ */
10
+ export class SingleAliasToken<T = any> extends BindableToken<T> {
11
+ select<R>(fn: (target: T) => R) {
12
+ return (s: IContainer) => fn(this.resolve(s));
13
+ }
14
+
15
+ /**
16
+ * @throws {DependencyNotFoundError} when no accessible registration carries the alias.
17
+ */
18
+ resolve(s: IContainer, options?: ResolveOptions): T {
19
+ return s.resolveOneByAlias(this.token, this.toResolveOptions(s, options));
20
+ }
21
+
22
+ protected bindKey(r: IRegistration<T>) {
23
+ r.bindToAlias(this.token);
24
+ }
25
+ }
26
+
27
+ /** Creates a {@link SingleAliasToken} for `token`. */
28
+ export const toSingleAlias = <T>(token: DependencyKey) => new SingleAliasToken<T>(token);
@@ -0,0 +1,35 @@
1
+ import { type IContainer } from '../container/IContainer';
2
+ import { BindableToken } from './BindableToken';
3
+ import { type IRegistration } from '../registration/IRegistration';
4
+ import { type ResolveOptions } from '../provider/IProvider';
5
+
6
+ /**
7
+ * A typed handle for one dependency key - the usual way to name a dependency.
8
+ * `args(...)`, `argsFn(...)` and `lazy()` return a new token and never change
9
+ * this one, so one token can be specialized per injection site.
10
+ *
11
+ * @example
12
+ * const ILoggerToken = new SingleToken<ILogger>('ILogger');
13
+ *
14
+ * @register(ILoggerToken)
15
+ * class Logger implements ILogger {}
16
+ *
17
+ * class App {
18
+ * constructor(@inject(by(ILoggerToken)) logger: ILogger) {}
19
+ * }
20
+ * ILoggerToken.resolve(scope);
21
+ * ILoggerToken.args('prefix').lazy().resolve(scope);
22
+ */
23
+ export class SingleToken<T = any> extends BindableToken<T> {
24
+ select<R>(fn: (target: T) => R) {
25
+ return (s: IContainer) => fn(this.resolve(s));
26
+ }
27
+
28
+ resolve(s: IContainer, options?: ResolveOptions): T {
29
+ return s.resolve(this.token, this.toResolveOptions(s, options));
30
+ }
31
+
32
+ protected bindKey(r: IRegistration<T>) {
33
+ r.bindToKey(this.token);
34
+ }
35
+ }
@@ -0,0 +1,27 @@
1
+ import { type FeatureEvaluation, MultiVariantFeatureToken } from './MultiVariantFeatureToken';
2
+ import { type FeatureContext, type IFeatureFlags } from '../feature/IFeatureFlags';
3
+
4
+ /**
5
+ * The on/off case of {@link MultiVariantFeatureToken}: a flag with no named
6
+ * variants, only a primary one, served while `isEnabled` says so. Everything
7
+ * else - the fallback, the context, args and laziness - is the multi-variant
8
+ * token's.
9
+ *
10
+ * @example
11
+ * const PaymentGatewayToken = new ToggleFeatureToken<IPaymentGateway>('new-checkout');
12
+ *
13
+ * @register(new SingleToken<IPaymentGateway>('LegacyGateway').fallbackOf(PaymentGatewayToken))
14
+ * class LegacyGateway implements IPaymentGateway {}
15
+ *
16
+ * @register(new SingleToken<IPaymentGateway>('StripeGateway').primaryVariantOf(PaymentGatewayToken))
17
+ * class StripeGateway implements IPaymentGateway {}
18
+ *
19
+ * class Checkout {
20
+ * constructor(@inject(by(PaymentGatewayToken)) gateway: IPaymentGateway) {}
21
+ * }
22
+ */
23
+ export class ToggleFeatureToken<T = any> extends MultiVariantFeatureToken<T, never> {
24
+ protected override evaluate(flags: IFeatureFlags, context: FeatureContext): FeatureEvaluation<never> {
25
+ return { enabled: flags.isEnabled(this.flag, context) };
26
+ }
27
+ }
@@ -0,0 +1,46 @@
1
+ import { DependencyKey, isDependencyKey } from '../container/IContainer';
2
+ import { SingleToken } from './SingleToken';
3
+ import { ClassToken } from './ClassToken';
4
+ import { FunctionToken } from './FunctionToken';
5
+ import { UnsupportedTokenTypeError } from '../errors/UnsupportedTokenTypeError';
6
+ import { InjectionToken, isInjectionToken } from './InjectionToken';
7
+ import { InjectFn } from '../hooks/hook';
8
+ import { type constructor, Is } from '../utils/basic';
9
+ import { ConstantToken } from './ConstantToken';
10
+
11
+ /** Anything `toToken(...)` turns into a token: a token, a key, a class or an `InjectFn`. */
12
+ export type Injectable<T = any> = InjectFn<T> | InjectionToken<T> | DependencyKey | constructor<T>;
13
+
14
+ /**
15
+ * @throws {UnsupportedTokenTypeError} when `token` is not an `InjectionToken`, a `DependencyKey`, a constructor, or a function.
16
+ */
17
+ export const toToken = <T = any>(token: Injectable<T>): InjectionToken<T> => {
18
+ if (token instanceof InjectionToken) {
19
+ return token;
20
+ }
21
+
22
+ if (isDependencyKey(token)) {
23
+ return new SingleToken(token);
24
+ }
25
+
26
+ if (Is.constructor(token)) {
27
+ return new ClassToken(token);
28
+ }
29
+
30
+ if (typeof token === 'function') {
31
+ return new FunctionToken(token as InjectFn<T>);
32
+ }
33
+
34
+ throw new UnsupportedTokenTypeError(
35
+ `Unknown token ${token}: expected an InjectionToken, a DependencyKey (string | symbol), a class or a function.`,
36
+ );
37
+ };
38
+
39
+ /**
40
+ * Wraps a runtime argument so it can be resolved uniformly: an `InjectionToken`
41
+ * is returned as-is, anything else becomes a `ConstantToken` of itself. The
42
+ * library never applies it to the args list - a call site that wants
43
+ * "resolve tokens, pass literals through" does so explicitly, e.g.
44
+ * `@inject(({ scope, args = [] }) => argToToken(args[0]).resolve(scope))`.
45
+ */
46
+ export const argToToken = <T = unknown>(v: T): InjectionToken<T> => (isInjectionToken<T>(v) ? v : new ConstantToken(v));
@@ -0,0 +1,149 @@
1
+ type ProxyState<T extends object> = {
2
+ getTarget: () => T;
3
+ };
4
+
5
+ /**
6
+ * The proxy operations the library depends on. Depend on this rather than on
7
+ * {@link ProxyRegistry} itself, so a caller can substitute its own
8
+ * implementation - the concrete registry is a singleton and cannot be
9
+ * constructed a second time.
10
+ */
11
+ export interface IProxyRegistry {
12
+ /**
13
+ * Returns the real object behind `value`, unwrapping proxies stacked to any
14
+ * depth. A value that is not a proxy is returned as is.
15
+ *
16
+ * The library calls this itself wherever a proxy would give the wrong answer -
17
+ * every read of class metadata (see `resolveConstructor`, `getHooks`,
18
+ * `resolveArgs`) and every identity check against a tracked instance (see
19
+ * `IContainer.hasInstance`). Pass the container's own values to those APIs as
20
+ * they come; unwrapping by hand is for code that needs the real object for its
21
+ * own reasons.
22
+ */
23
+ unwrap<T extends object>(value: T): T;
24
+
25
+ createProxy<T extends object>(target: T, handler?: ProxyHandler<T>): T;
26
+
27
+ /**
28
+ * Creates a proxy that resolves its target on first access and caches it.
29
+ */
30
+ createLazyProxy<T extends object>(resolveInstance: () => T): T;
31
+
32
+ /**
33
+ * Wraps `resolveInstance` in a lazy proxy when `isLazy`, otherwise calls it
34
+ * right away.
35
+ */
36
+ toLazyIf<T extends object>(resolveInstance: () => T, isLazy?: boolean): T;
37
+ }
38
+
39
+ /**
40
+ * Creates proxies and tracks them, so a proxy can be traced back to the real
41
+ * object it stands for. The library's own {@link IProxyRegistry}.
42
+ *
43
+ * Process-wide singleton: a proxy created anywhere must be unwrappable
44
+ * everywhere, and the registry holds no configuration that would justify a
45
+ * second instance. Obtain it with {@link ProxyRegistry.getInstance}.
46
+ *
47
+ * Entries are held weakly - a proxy that becomes unreachable is collected along
48
+ * with its state.
49
+ */
50
+ export class ProxyRegistry implements IProxyRegistry {
51
+ private static instance?: ProxyRegistry;
52
+
53
+ private readonly states = new WeakMap<object, ProxyState<object>>();
54
+
55
+ private constructor() {}
56
+
57
+ static getInstance(): ProxyRegistry {
58
+ ProxyRegistry.instance ??= new ProxyRegistry();
59
+ return ProxyRegistry.instance;
60
+ }
61
+
62
+ /**
63
+ * Returns the real object behind `value`, unwrapping proxies stacked to any
64
+ * depth. A value that is not a proxy is returned as is.
65
+ */
66
+ unwrap<T extends object>(value: T): T {
67
+ return this.isProxy(value) ? this.getTarget(value) : value;
68
+ }
69
+
70
+ createProxy<T extends object>(target: T, handler: ProxyHandler<T> = {}): T {
71
+ const proxy = new Proxy(target, handler);
72
+ return this.register(proxy, { getTarget: () => target });
73
+ }
74
+
75
+ /**
76
+ * Creates a proxy that resolves its target on first access and caches it.
77
+ * The target is unwrapped on resolution, so wrapping a lazy proxy in another
78
+ * lazy proxy never stacks beyond one level.
79
+ */
80
+ createLazyProxy<T extends object>(resolveInstance: () => T): T {
81
+ let instance: T | undefined;
82
+ const state: ProxyState<T> = {
83
+ getTarget: () => {
84
+ instance = instance ?? this.unwrap(resolveInstance());
85
+ return instance;
86
+ },
87
+ };
88
+
89
+ const proxy = new Proxy(
90
+ {},
91
+ {
92
+ get: (_, prop) => {
93
+ const target = state.getTarget();
94
+ // @ts-ignore
95
+ return target[prop];
96
+ },
97
+ // The proxy stands in for the target, so writes must reach it. Without this
98
+ // the empty object above would absorb them - including writes a method makes
99
+ // to `this` when it is called through the proxy.
100
+ set: (_, prop, value) => {
101
+ const target = state.getTarget();
102
+ // @ts-ignore
103
+ target[prop] = value;
104
+ return true;
105
+ },
106
+ },
107
+ ) as T;
108
+
109
+ return this.register(proxy, state);
110
+ }
111
+
112
+ /**
113
+ * Wraps `resolveInstance` in a lazy proxy when `isLazy`, otherwise calls it
114
+ * right away.
115
+ */
116
+ toLazyIf<T extends object>(resolveInstance: () => T, isLazy: boolean = false): T {
117
+ return isLazy ? this.createLazyProxy(resolveInstance) : resolveInstance();
118
+ }
119
+
120
+ private isProxy(value: object): boolean {
121
+ return this.states.has(value);
122
+ }
123
+
124
+ private getTarget<T extends object>(value: T): T {
125
+ const target = this.states.get(value)!.getTarget() as T;
126
+ return this.unwrap(target);
127
+ }
128
+
129
+ private register<T extends object>(proxy: T, state: ProxyState<T>): T {
130
+ this.states.set(proxy, state as ProxyState<object>);
131
+ return proxy;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * The real object behind `value`, unwrapping proxies stacked to any depth. A
137
+ * value that is not a proxy is returned as is.
138
+ *
139
+ * A shortcut for `ProxyRegistry.getInstance().unwrap(value)` - the registry is a
140
+ * process-wide singleton, so unwrapping never needs an instance of its own. The
141
+ * library already unwraps wherever a proxy would give the wrong answer (reading
142
+ * class metadata, and `IContainer.hasInstance`), so reach for this only when
143
+ * your own code needs the real object - an identity check of your own, or
144
+ * bypassing a lazy proxy on purpose.
145
+ *
146
+ * Note that unwrapping a lazy proxy resolves its target, which is the point:
147
+ * there is no real object to hand back until it does.
148
+ */
149
+ export const unwrapProxy = <T extends object>(value: T): T => ProxyRegistry.getInstance().unwrap(value);
@@ -0,0 +1,96 @@
1
+ import { TypedEventDisposedError } from '../errors/TypedEventDisposedError';
2
+
3
+ export type TypedEventListener<TArgs extends unknown[]> = (...args: TArgs) => void;
4
+
5
+ export type Unsubscribe = () => void;
6
+
7
+ /**
8
+ * The subscriber's side of a {@link TypedEvent}: attach, detach, tear down.
9
+ * Hand this out instead of the event itself so that only the owner can `emit`.
10
+ */
11
+ export interface ITypedEvent<TArgs extends unknown[]> {
12
+ /**
13
+ * Attaches `listener` and returns a function which detaches it again. A
14
+ * listener subscribed more than once is still delivered each emission once.
15
+ *
16
+ * @throws {TypedEventDisposedError} when the event has already been disposed.
17
+ */
18
+ subscribe(listener: TypedEventListener<TArgs>): Unsubscribe;
19
+
20
+ /**
21
+ * Detaches `listener`. Harmless when it is not subscribed, or after `dispose`.
22
+ */
23
+ unsubscribe(listener: TypedEventListener<TArgs>): void;
24
+
25
+ /**
26
+ * Detaches every listener and rejects further `subscribe` / `emit` calls.
27
+ */
28
+ dispose(): void;
29
+ }
30
+
31
+ /**
32
+ * A typed, multi-listener event. `TArgs` is the argument list a listener
33
+ * receives, so a `TypedEvent<[IContainer]>` accepts `(scope: IContainer) => void`
34
+ * listeners unchanged.
35
+ *
36
+ * Emission delivers to the listeners subscribed at the moment `emit` is called,
37
+ * in subscription order. A listener detached mid-emission is skipped for the
38
+ * rest of that emission; one attached mid-emission receives only later ones.
39
+ */
40
+ export class TypedEvent<TArgs extends unknown[] = []> implements ITypedEvent<TArgs> {
41
+ private readonly listeners = new Set<TypedEventListener<TArgs>>();
42
+ private isDisposed = false;
43
+
44
+ /**
45
+ * @throws {TypedEventDisposedError} when the event has already been disposed.
46
+ */
47
+ subscribe(listener: TypedEventListener<TArgs>): Unsubscribe {
48
+ this.validate();
49
+ this.listeners.add(listener);
50
+ return () => this.unsubscribe(listener);
51
+ }
52
+
53
+ unsubscribe(listener: TypedEventListener<TArgs>): void {
54
+ this.listeners.delete(listener);
55
+ }
56
+
57
+ /**
58
+ * Calls every current listener with `args`. What a listener throws surfaces
59
+ * out of `emit` and stops delivery to the listeners after it.
60
+ *
61
+ * @throws {TypedEventDisposedError} when the event has already been disposed.
62
+ * @throws {unknown} rethrows whatever a listener threw.
63
+ */
64
+ emit(...args: TArgs): void {
65
+ this.validate();
66
+ // Iterate a snapshot so a listener can (un)subscribe without disturbing this emission;
67
+ // a listener detached mid-emission is still skipped for the rest of it.
68
+ for (const listener of [...this.listeners]) {
69
+ if (this.listeners.has(listener)) {
70
+ listener(...args);
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * A copy of the current listeners, in subscription order.
77
+ */
78
+ getListeners(): TypedEventListener<TArgs>[] {
79
+ return [...this.listeners];
80
+ }
81
+
82
+ dispose(): void {
83
+ this.listeners.clear();
84
+ this.isDisposed = true;
85
+ }
86
+
87
+ /**
88
+ * @throws {TypedEventDisposedError} when the event has already been disposed.
89
+ */
90
+ private validate(): void {
91
+ TypedEventDisposedError.assert(
92
+ !this.isDisposed,
93
+ 'TypedEvent is already disposed: its scope was disposed, so it accepts no new listeners.',
94
+ );
95
+ }
96
+ }
@@ -0,0 +1,28 @@
1
+ import { ArgumentNotFoundError } from '../errors/ArgumentNotFoundError';
2
+
3
+ export type Predicate<T> = (value: T) => boolean;
4
+
5
+ export const Filter = {
6
+ exclude: <T>(arr: Set<T> | T[]) => {
7
+ const excludeSet = arr instanceof Array ? new Set(arr) : arr;
8
+ return (v: T) => !excludeSet.has(v);
9
+ },
10
+ };
11
+
12
+ /**
13
+ * Returns a picker of the first argument in `args` matching `predicate` - the
14
+ * shape `singleton(getCacheKey)` consumers expect.
15
+ *
16
+ * @throws {ArgumentNotFoundError} when no argument matches `predicate`
17
+ */
18
+ export const findOrFail =
19
+ <T>(predicate: Predicate<T>) =>
20
+ (args: unknown[] = []): T => {
21
+ const index = args.findIndex((arg) => predicate(arg as T));
22
+
23
+ if (index === -1) {
24
+ throw new ArgumentNotFoundError(`No argument matches the predicate (received ${args.length} args).`);
25
+ }
26
+
27
+ return args[index] as T;
28
+ };
@@ -0,0 +1,34 @@
1
+ /** A class that constructs `T`. */
2
+ export type constructor<T> = new (...args: any[]) => T;
3
+
4
+ /** Something with a meaningful `toString()`, e.g. a token used as a cache key. */
5
+ export interface Serializable {
6
+ toString(): string;
7
+ }
8
+
9
+ /**
10
+ * Tags a structural type with a name, so `Instance` reads as its own type in
11
+ * signatures and error messages instead of a bare `object`. The tag is optional
12
+ * on purpose: values stay assignable without a cast.
13
+ */
14
+ export type Branded<TBrand extends string, T> = T & { readonly __brand?: TBrand };
15
+
16
+ /**
17
+ * What the container tracks and reflects on: the object a constructor produced.
18
+ * Deliberately not a constructor signature - an instance is not its own constructor.
19
+ */
20
+ export type Instance = Branded<'Instance', object>;
21
+
22
+ export const Is = {
23
+ nullish: <T>(value: T | undefined | null): value is null | undefined => value === undefined || value === null,
24
+ object: (target: unknown): target is object => target !== null && typeof target === 'object',
25
+ instance: (target: unknown): target is Instance => Object.prototype.hasOwnProperty.call(target, 'constructor'),
26
+ constructor: (target: unknown): target is constructor<unknown> => typeof target === 'function' && !!target.prototype,
27
+ };
28
+
29
+ // Every object inherits `Object.prototype.toString`, so only an overridden one counts as serialization.
30
+ export function isSerializable(target: unknown): target is Serializable {
31
+ return Is.object(target) && typeof target.toString === 'function' && target.toString !== Object.prototype.toString;
32
+ }
33
+
34
+ export const toString = (target: unknown): string => (isSerializable(target) ? target.toString() : String(target));
@@ -0,0 +1,18 @@
1
+ /** Method decorator: delays the call until `ms` passed without another call (per instance). */
2
+ export const debounce =
3
+ (ms: number): MethodDecorator =>
4
+ (target, propertyKey, descriptor: PropertyDescriptor) => {
5
+ const originalMethod = descriptor.value;
6
+ const timerMap = new WeakMap<object, ReturnType<typeof setTimeout>>();
7
+ descriptor.value = function (...args: unknown[]) {
8
+ const prev = timerMap.get(this);
9
+ if (prev !== undefined) {
10
+ clearTimeout(prev);
11
+ }
12
+ timerMap.set(
13
+ this,
14
+ setTimeout(() => originalMethod.apply(this, args), ms),
15
+ );
16
+ };
17
+ return descriptor;
18
+ };
@@ -0,0 +1,34 @@
1
+ import { resolveConstructor } from '../metadata/target';
2
+
3
+ /** Receives the error with the class and method it came from. */
4
+ export type HandleErrorParams = (error: unknown, context: { target: string; method: string }) => void;
5
+
6
+ /** Method decorator: routes a rejection of the async method to `errorHandler` instead of throwing. */
7
+ export const handleAsyncError =
8
+ (errorHandler: HandleErrorParams): MethodDecorator =>
9
+ (target, propertyKey, descriptor: PropertyDescriptor) => {
10
+ const originalMethod = descriptor.value;
11
+ descriptor.value = async function (...args: unknown[]) {
12
+ try {
13
+ return await originalMethod.apply(this, args);
14
+ } catch (e) {
15
+ errorHandler(e, { target: resolveConstructor(target).name, method: propertyKey as string });
16
+ }
17
+ };
18
+ return descriptor;
19
+ };
20
+
21
+ /** Method decorator: routes a throw of the method to `errorHandler` instead of throwing. */
22
+ export const handleError =
23
+ (errorHandler: HandleErrorParams): MethodDecorator =>
24
+ (target, propertyKey, descriptor: PropertyDescriptor) => {
25
+ const originalMethod = descriptor.value;
26
+ descriptor.value = function (...args: unknown[]) {
27
+ try {
28
+ return originalMethod.apply(this, args);
29
+ } catch (e) {
30
+ errorHandler(e, { target: resolveConstructor(target).name, method: propertyKey as string });
31
+ }
32
+ };
33
+ return descriptor;
34
+ };