composed-di 0.2.9-ts4 → 0.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 (46) hide show
  1. package/README.md +182 -141
  2. package/dist/errors.d.ts +17 -0
  3. package/dist/errors.d.ts.map +1 -0
  4. package/dist/errors.js +26 -0
  5. package/dist/errors.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/serviceFactory.d.ts +6 -5
  11. package/dist/serviceFactory.d.ts.map +1 -1
  12. package/dist/serviceFactory.js +22 -7
  13. package/dist/serviceFactory.js.map +1 -1
  14. package/dist/serviceFactoryWrapper.d.ts +2 -0
  15. package/dist/serviceFactoryWrapper.d.ts.map +1 -0
  16. package/dist/serviceFactoryWrapper.js +16 -0
  17. package/dist/serviceFactoryWrapper.js.map +1 -0
  18. package/dist/serviceKey.d.ts +84 -0
  19. package/dist/serviceKey.d.ts.map +1 -1
  20. package/dist/serviceKey.js +83 -2
  21. package/dist/serviceKey.js.map +1 -1
  22. package/dist/serviceModule.d.ts +26 -5
  23. package/dist/serviceModule.d.ts.map +1 -1
  24. package/dist/serviceModule.js +106 -15
  25. package/dist/serviceModule.js.map +1 -1
  26. package/dist/serviceSelector.d.ts +64 -0
  27. package/dist/serviceSelector.d.ts.map +1 -0
  28. package/dist/serviceSelector.js +69 -0
  29. package/dist/serviceSelector.js.map +1 -0
  30. package/dist/test-service-selector.d.ts +2 -0
  31. package/dist/test-service-selector.d.ts.map +1 -0
  32. package/dist/test-service-selector.js +110 -0
  33. package/dist/test-service-selector.js.map +1 -0
  34. package/dist/utils.d.ts +33 -6
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +100 -6
  37. package/dist/utils.js.map +1 -1
  38. package/package.json +45 -41
  39. package/src/errors.ts +23 -0
  40. package/src/index.ts +7 -5
  41. package/src/serviceFactory.ts +104 -83
  42. package/src/serviceKey.ts +95 -8
  43. package/src/serviceModule.ts +223 -123
  44. package/src/serviceScope.ts +7 -7
  45. package/src/serviceSelector.ts +68 -0
  46. package/src/utils.ts +277 -152
@@ -1,123 +1,223 @@
1
- import { ServiceKey } from './serviceKey';
2
- import { ServiceFactory } from './serviceFactory';
3
- import { ServiceScope } from './serviceScope';
4
-
5
- type GenericFactory = ServiceFactory<any, readonly ServiceKey<any>[]>;
6
- type GenericKey = ServiceKey<any>;
7
-
8
- export class ServiceModule {
9
- private constructor(readonly factories: GenericFactory[]) {
10
- factories.forEach((factory) => {
11
- checkRecursiveDependencies(factory);
12
- checkMissingDependencies(factory, this.factories);
13
- });
14
- }
15
-
16
- public async get<T>(key: ServiceKey<T>): Promise<T> {
17
- const factory = this.factories.find((factory: GenericFactory) => {
18
- return isSuitable(key, factory);
19
- });
20
-
21
- // Check if a factory to supply the requested key was not found
22
- if (!factory) {
23
- throw new Error(`Could not find a suitable factory for ${key.name}`);
24
- }
25
-
26
- // Resolve all dependencies first
27
- const dependencies = await Promise.all(
28
- factory.dependsOn.map((dependencyKey: ServiceKey<unknown>) => {
29
- return this.get(dependencyKey);
30
- }),
31
- );
32
-
33
- // Call the factory to retrieve the dependency
34
- return factory.initialize(...dependencies);
35
- }
36
-
37
- /**
38
- * Disposes of service factories within the specified scope or all factories if no scope is provided.
39
- *
40
- * This method is useful for cleaning up resources and instances held by service factories,
41
- * such as singleton factories, as they may hold database connections or other resources that need to be released.
42
- *
43
- * @param {ServiceScope} [scope] The scope to filter the factories to be disposed.
44
- * If not provided, all factories are disposed of.
45
- * @return {void} No return value.
46
- */
47
- public dispose(scope?: ServiceScope) {
48
- const factories = scope
49
- ? this.factories.filter((f) => f.scope === scope)
50
- : this.factories;
51
-
52
- factories.forEach((factory) => factory.dispose?.());
53
- }
54
-
55
- /**
56
- * Creates a new ServiceModule instance by aggregating and deduplicating a list of
57
- * ServiceModule or GenericFactory instances.
58
- * If multiple factories provide the same
59
- * ServiceKey, the last one in the list takes precedence.
60
- *
61
- * @param {Array<ServiceModule | GenericFactory>} entries - An array of ServiceModule or GenericFactory
62
- * instances to be processed into a single ServiceModule.
63
- * @return {ServiceModule} A new ServiceModule containing the deduplicated factories.
64
- */
65
- static from(entries: (ServiceModule | GenericFactory)[]): ServiceModule {
66
- // Flatten entries and keep only the last factory for each ServiceKey
67
- const flattened = entries.flatMap((e) =>
68
- e instanceof ServiceModule ? e.factories : [e],
69
- );
70
-
71
- const byKey = new Map<symbol, GenericFactory>();
72
- // Later factories overwrite earlier ones (last-wins)
73
- for (const f of flattened) {
74
- byKey.set(f.provides.symbol, f);
75
- }
76
-
77
- return new ServiceModule(Array.from(byKey.values()));
78
- }
79
- }
80
-
81
- function checkRecursiveDependencies(factory: GenericFactory) {
82
- const recursive = factory.dependsOn.some((dependencyKey) => {
83
- return dependencyKey === factory.provides;
84
- });
85
-
86
- if (recursive) {
87
- throw new Error(
88
- 'Recursive dependency detected on: ' + factory.provides.name,
89
- );
90
- }
91
- }
92
-
93
- function checkMissingDependencies(
94
- factory: GenericFactory,
95
- factories: GenericFactory[],
96
- ) {
97
- const missingDependencies = factory.dependsOn.filter(
98
- (dependencyKey: GenericKey) => {
99
- return !isRegistered(dependencyKey, factories);
100
- },
101
- );
102
- if (missingDependencies.length === 0) {
103
- return;
104
- }
105
-
106
- const dependencyList = missingDependencies
107
- .map((dependencyKey) => ` -> ${dependencyKey.name}`)
108
- .join('\n');
109
- throw new Error(
110
- `${factory.provides.name} will fail because it depends on:\n ${dependencyList}`,
111
- );
112
- }
113
-
114
- function isRegistered(key: GenericKey, factories: GenericFactory[]) {
115
- return factories.some((factory) => factory.provides?.symbol === key?.symbol);
116
- }
117
-
118
- function isSuitable<T, D extends readonly ServiceKey<any>[]>(
119
- key: ServiceKey<T>,
120
- factory: ServiceFactory<any, D>,
121
- ): factory is ServiceFactory<T, D> {
122
- return factory?.provides?.symbol === key?.symbol;
123
- }
1
+ import { ServiceKey, ServiceSelectorKey } from './serviceKey';
2
+ import { ServiceFactory } from './serviceFactory';
3
+ import { ServiceScope } from './serviceScope';
4
+ import { ServiceSelector } from './serviceSelector';
5
+ import { ServiceFactoryNotFoundError, ServiceModuleInitError } from './errors';
6
+
7
+ type GenericFactory = ServiceFactory<unknown, readonly ServiceKey<any>[]>;
8
+ type GenericKey = ServiceKey<any>;
9
+
10
+ /**
11
+ * ServiceModule is a container for service factories and manages dependency resolution.
12
+ *
13
+ * It provides a way to retrieve service instances based on their ServiceKey,
14
+ * ensuring that all dependencies are resolved and initialized in the correct order.
15
+ * It also handles circular dependency detection and missing dependency validation
16
+ * at the time of module creation.
17
+ */
18
+ export class ServiceModule {
19
+ /**
20
+ * Private constructor to enforce module creation through the `static from` method.
21
+ *
22
+ * @param factories An array of service factories that this module will manage.
23
+ */
24
+ private constructor(readonly factories: GenericFactory[]) {
25
+ checkCircularDependencies(this.factories);
26
+ factories.forEach((factory) => {
27
+ checkMissingDependencies(factory, this.factories);
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Retrieves an instance for the given ServiceKey.
33
+ *
34
+ * @param key - The key of the service to retrieve.
35
+ * @return A promise that resolves to the service instance.
36
+ * @throws {ServiceFactoryNotFoundError} If no suitable factory is found for the given key.
37
+ */
38
+ public async get<T>(key: ServiceKey<T>): Promise<T> {
39
+ const factory = this.factories.find((factory: GenericFactory) => {
40
+ return isSuitable(key, factory);
41
+ });
42
+
43
+ // Check if a factory to supply the requested key was not found
44
+ if (!factory) {
45
+ throw new ServiceFactoryNotFoundError(
46
+ `Could not find a suitable factory for ${key.name}`,
47
+ );
48
+ }
49
+
50
+ // Resolve all dependencies first
51
+ const dependencies = await Promise.all(
52
+ factory.dependsOn.map((dependencyKey: ServiceKey<unknown>) => {
53
+ // If the dependency is a ServiceSelectorKey, create a ServiceSelector instance
54
+ if (dependencyKey instanceof ServiceSelectorKey) {
55
+ return new ServiceSelector(this, dependencyKey);
56
+ }
57
+ return this.get(dependencyKey);
58
+ }),
59
+ );
60
+
61
+ // Call the factory to retrieve the dependency
62
+ return factory.initialize(...dependencies);
63
+ }
64
+
65
+ /**
66
+ * Disposes of service factories within the specified scope or all factories if no scope is provided.
67
+ *
68
+ * This method is useful for cleaning up resources and instances held by service factories,
69
+ * such as singleton factories, as they may hold database connections or other resources that need to be released.
70
+ *
71
+ * @param scope The scope to filter the factories to be disposed.
72
+ * If not provided, all factories are disposed of.
73
+ * @return No return value.
74
+ */
75
+ public dispose(scope?: ServiceScope) {
76
+ const factories = scope
77
+ ? this.factories.filter((f) => f.scope === scope)
78
+ : this.factories;
79
+
80
+ factories.forEach((factory) => factory.dispose?.());
81
+ }
82
+
83
+ /**
84
+ * Creates a new ServiceModule instance by aggregating and deduplicating a list of
85
+ * ServiceModule or GenericFactory instances.
86
+ * If multiple factories provide the same
87
+ * ServiceKey, the last one in the list takes precedence.
88
+ *
89
+ * @param entries - An array of ServiceModule or GenericFactory
90
+ * instances to be processed into a single ServiceModule.
91
+ * @return A new ServiceModule containing the deduplicated factories.
92
+ * @throws {ServiceModuleInitError} If circular or missing dependencies are detected during module creation.
93
+ */
94
+ static from(entries: (ServiceModule | GenericFactory)[]): ServiceModule {
95
+ // Flatten entries and keep only the last factory for each ServiceKey
96
+ const flattened = entries.flatMap((e) =>
97
+ e instanceof ServiceModule ? e.factories : [e],
98
+ );
99
+
100
+ const byKey = new Map<symbol, GenericFactory>();
101
+ // Later factories overwrite earlier ones (last-wins)
102
+ for (const f of flattened) {
103
+ byKey.set(f.provides.symbol, f);
104
+ }
105
+
106
+ return new ServiceModule(Array.from(byKey.values()));
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Validates that there are no circular dependencies among the provided factories.
112
+ *
113
+ * @param factories The list of factories to check for cycles.
114
+ * @throws {ServiceModuleInitError} If a circular dependency is detected.
115
+ */
116
+ function checkCircularDependencies(factories: GenericFactory[]) {
117
+ const factoryMap = new Map<symbol, GenericFactory>();
118
+ for (const f of factories) {
119
+ factoryMap.set(f.provides.symbol, f);
120
+ }
121
+
122
+ const visited = new Set<symbol>();
123
+ const stack = new Set<symbol>();
124
+
125
+ function walk(factory: GenericFactory, path: string[]) {
126
+ const symbol = factory.provides.symbol;
127
+
128
+ if (stack.has(symbol)) {
129
+ const cyclePath = [...path, factory.provides.name].join(' -> ');
130
+ throw new ServiceModuleInitError(
131
+ `Circular dependency detected: ${cyclePath}`,
132
+ );
133
+ }
134
+
135
+ if (visited.has(symbol)) {
136
+ return;
137
+ }
138
+
139
+ visited.add(symbol);
140
+ stack.add(symbol);
141
+
142
+ for (const depKey of factory.dependsOn) {
143
+ const keysToCheck =
144
+ depKey instanceof ServiceSelectorKey ? depKey.values : [depKey];
145
+
146
+ for (const key of keysToCheck) {
147
+ const depFactory = factoryMap.get(key.symbol);
148
+ if (depFactory) {
149
+ walk(depFactory, [...path, factory.provides.name]);
150
+ }
151
+ }
152
+ }
153
+
154
+ stack.delete(symbol);
155
+ }
156
+
157
+ for (const factory of factories) {
158
+ walk(factory, []);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Validates that all dependencies of a given factory are present in the list of factories.
164
+ *
165
+ * @param factory The factory whose dependencies are to be checked.
166
+ * @param factories The list of available factories in the module.
167
+ * @throws {ServiceModuleInitError} If any dependency is missing.
168
+ */
169
+ function checkMissingDependencies(
170
+ factory: GenericFactory,
171
+ factories: GenericFactory[],
172
+ ) {
173
+ const missingDependencies: GenericKey[] = [];
174
+
175
+ factory.dependsOn.forEach((dependencyKey: GenericKey) => {
176
+ // For ServiceSelectorKey, check all contained keys are registered
177
+ if (dependencyKey instanceof ServiceSelectorKey) {
178
+ dependencyKey.values.forEach((key) => {
179
+ if (!isRegistered(key, factories)) {
180
+ missingDependencies.push(key);
181
+ }
182
+ });
183
+ } else if (!isRegistered(dependencyKey, factories)) {
184
+ missingDependencies.push(dependencyKey);
185
+ }
186
+ });
187
+
188
+ if (missingDependencies.length === 0) {
189
+ return;
190
+ }
191
+
192
+ const dependencyList = missingDependencies
193
+ .map((dependencyKey) => ` -> ${dependencyKey.name}`)
194
+ .join('\n');
195
+ throw new ServiceModuleInitError(
196
+ `${factory.provides.name} will fail because it depends on:\n ${dependencyList}`,
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Checks if a ServiceKey is registered among the provided factories.
202
+ *
203
+ * @param key The ServiceKey to look for.
204
+ * @param factories The list of factories to search in.
205
+ * @returns True if a factory provides the given key, false otherwise.
206
+ */
207
+ function isRegistered(key: GenericKey, factories: GenericFactory[]) {
208
+ return factories.some((factory) => factory.provides?.symbol === key?.symbol);
209
+ }
210
+
211
+ /**
212
+ * Determines if a factory is suitable for providing a specific ServiceKey.
213
+ *
214
+ * @param key The ServiceKey being requested.
215
+ * @param factory The factory to check.
216
+ * @returns True if the factory provides the key, false otherwise.
217
+ */
218
+ function isSuitable<T, D extends readonly ServiceKey<any>[]>(
219
+ key: ServiceKey<T>,
220
+ factory: ServiceFactory<any, D>,
221
+ ): factory is ServiceFactory<T, D> {
222
+ return factory?.provides?.symbol === key?.symbol;
223
+ }
@@ -1,7 +1,7 @@
1
- export class ServiceScope {
2
- readonly symbol: symbol;
3
-
4
- constructor(readonly name: string) {
5
- this.symbol = Symbol(name);
6
- }
7
- }
1
+ export class ServiceScope {
2
+ readonly symbol: symbol;
3
+
4
+ constructor(readonly name: string) {
5
+ this.symbol = Symbol(name);
6
+ }
7
+ }
@@ -0,0 +1,68 @@
1
+ import { ServiceKey, ServiceSelectorKey } from './serviceKey';
2
+ import { ServiceModule } from './serviceModule';
3
+
4
+ /**
5
+ * A runtime selector that provides access to multiple service implementations of the same type.
6
+ *
7
+ * ServiceSelector is automatically created and injected when a factory depends on a
8
+ * `ServiceSelectorKey<T>`. It allows the dependent service to dynamically choose which
9
+ * implementation to use at runtime, rather than being bound to a single implementation
10
+ * at configuration time.
11
+ *
12
+ * @template T The common type shared by all services accessible through this selector.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // In a factory that depends on ServiceSelectorKey
17
+ * const appFactory = ServiceFactory.singleton({
18
+ * provides: AppKey,
19
+ * dependsOn: [LoggerSelectorKey] as const,
20
+ * initialize: (loggerSelector: ServiceSelector<Logger>) => {
21
+ * return {
22
+ * logWithConsole: async () => {
23
+ * const logger = await loggerSelector.get(ConsoleLoggerKey);
24
+ * logger.log('Using console logger');
25
+ * },
26
+ * logWithFile: async () => {
27
+ * const logger = await loggerSelector.get(FileLoggerKey);
28
+ * logger.log('Using file logger');
29
+ * },
30
+ * };
31
+ * },
32
+ * });
33
+ * ```
34
+ */
35
+ export class ServiceSelector<T> {
36
+ /**
37
+ * Creates a new ServiceSelector instance.
38
+ *
39
+ * Note: ServiceSelector instances are created automatically by ServiceModule
40
+ * when resolving dependencies. You typically don't need to create them manually.
41
+ *
42
+ * @param serviceModule The ServiceModule used to resolve the selected service.
43
+ * @param selectorKey The ServiceSelectorKey that defines which services can be selected.
44
+ */
45
+ constructor(
46
+ readonly serviceModule: ServiceModule,
47
+ readonly selectorKey: ServiceSelectorKey<T>,
48
+ ) {}
49
+
50
+ /**
51
+ * Retrieves a service instance by its key from the available services in this selector.
52
+ *
53
+ * The key must be one of the keys that were included in the `ServiceSelectorKey`
54
+ * used to create this selector.
55
+ *
56
+ * @param key The ServiceKey identifying which service implementation to retrieve.
57
+ * @returns A Promise that resolves to the requested service instance.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const logger = await loggerSelector.get(ConsoleLoggerKey);
62
+ * logger.log('Hello!');
63
+ * ```
64
+ */
65
+ get(key: ServiceKey<T>): Promise<T> {
66
+ return this.serviceModule.get(key);
67
+ }
68
+ }