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.
- package/README.md +182 -141
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/serviceFactory.d.ts +6 -5
- package/dist/serviceFactory.d.ts.map +1 -1
- package/dist/serviceFactory.js +22 -7
- package/dist/serviceFactory.js.map +1 -1
- package/dist/serviceFactoryWrapper.d.ts +2 -0
- package/dist/serviceFactoryWrapper.d.ts.map +1 -0
- package/dist/serviceFactoryWrapper.js +16 -0
- package/dist/serviceFactoryWrapper.js.map +1 -0
- package/dist/serviceKey.d.ts +84 -0
- package/dist/serviceKey.d.ts.map +1 -1
- package/dist/serviceKey.js +83 -2
- package/dist/serviceKey.js.map +1 -1
- package/dist/serviceModule.d.ts +26 -5
- package/dist/serviceModule.d.ts.map +1 -1
- package/dist/serviceModule.js +106 -15
- package/dist/serviceModule.js.map +1 -1
- package/dist/serviceSelector.d.ts +64 -0
- package/dist/serviceSelector.d.ts.map +1 -0
- package/dist/serviceSelector.js +69 -0
- package/dist/serviceSelector.js.map +1 -0
- package/dist/test-service-selector.d.ts +2 -0
- package/dist/test-service-selector.d.ts.map +1 -0
- package/dist/test-service-selector.js +110 -0
- package/dist/test-service-selector.js.map +1 -0
- package/dist/utils.d.ts +33 -6
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +100 -6
- package/dist/utils.js.map +1 -1
- package/package.json +45 -41
- package/src/errors.ts +23 -0
- package/src/index.ts +7 -5
- package/src/serviceFactory.ts +104 -83
- package/src/serviceKey.ts +95 -8
- package/src/serviceModule.ts +223 -123
- package/src/serviceScope.ts +7 -7
- package/src/serviceSelector.ts +68 -0
- package/src/utils.ts +277 -152
package/src/serviceModule.ts
CHANGED
|
@@ -1,123 +1,223 @@
|
|
|
1
|
-
import { ServiceKey } from './serviceKey';
|
|
2
|
-
import { ServiceFactory } from './serviceFactory';
|
|
3
|
-
import { ServiceScope } from './serviceScope';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
package/src/serviceScope.ts
CHANGED
|
@@ -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
|
+
}
|