@tachybase/di 1.3.51 → 1.3.53

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/lib/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export type { ServiceMetadata } from './interfaces/service-metadata.interface';
10
10
  export type { ServiceOptions } from './interfaces/service-options.interface';
11
11
  export type { ServiceIdentifier } from './types/service-identifier.type';
12
12
  export { ContainerInstance, Container } from './container-instance.class';
13
+ export { ContainerRegistry } from './container-registry.class';
13
14
  export { Token } from './token.class';
14
15
  export default Container;
15
16
  export * from './decorators';
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@ var index_exports = {};
20
20
  __export(index_exports, {
21
21
  Container: () => import_container_instance2.Container,
22
22
  ContainerInstance: () => import_container_instance2.ContainerInstance,
23
+ ContainerRegistry: () => import_container_registry.ContainerRegistry,
23
24
  Token: () => import_token.Token,
24
25
  default: () => index_default
25
26
  });
@@ -32,6 +33,7 @@ __reExport(index_exports, require("./error/cannot-inject-value.error"), module.e
32
33
  __reExport(index_exports, require("./error/cannot-instantiate-value.error"), module.exports);
33
34
  __reExport(index_exports, require("./error/service-not-found.error"), module.exports);
34
35
  var import_container_instance2 = require("./container-instance.class");
36
+ var import_container_registry = require("./container-registry.class");
35
37
  var import_token = require("./token.class");
36
38
  __reExport(index_exports, require("./decorators"), module.exports);
37
39
  var index_default = import_container_instance.Container;
@@ -39,6 +41,7 @@ var index_default = import_container_instance.Container;
39
41
  0 && (module.exports = {
40
42
  Container,
41
43
  ContainerInstance,
44
+ ContainerRegistry,
42
45
  Token,
43
46
  ...require("./decorators/inject-many.decorator"),
44
47
  ...require("./decorators/inject.decorator"),
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@tachybase/di",
3
- "version": "1.3.51",
3
+ "version": "1.3.53",
4
4
  "description": "",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
8
8
  "dependencies": {
9
- "@tachybase/utils": "^1.3.42"
9
+ "@tachybase/utils": "^1.3.52"
10
10
  }
11
11
  }
@@ -0,0 +1,324 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { CannotInstantiateValueError, Container, ContainerInstance, ServiceNotFoundError, Token } from '../index';
4
+
5
+ describe('ContainerInstance', () => {
6
+ let container: ContainerInstance;
7
+
8
+ beforeEach(() => {
9
+ container = new ContainerInstance(`test-container-${Math.random()}`);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.reset({ strategy: 'resetServices' });
14
+ });
15
+
16
+ describe('Basic Operations', () => {
17
+ it('should create a new container instance with given id', () => {
18
+ const newContainer = new ContainerInstance('my-container');
19
+ expect(newContainer.id).toBe('my-container');
20
+ });
21
+
22
+ it('should have default container', () => {
23
+ expect(ContainerInstance.default).toBeDefined();
24
+ expect(ContainerInstance.default.id).toBe('default');
25
+ });
26
+
27
+ it('should check if service exists', () => {
28
+ class TestService {}
29
+
30
+ expect(container.has(TestService)).toBe(false);
31
+ container.set({ type: TestService });
32
+ expect(container.has(TestService)).toBe(true);
33
+ });
34
+
35
+ it('should register and retrieve service by class', () => {
36
+ class TestService {
37
+ public name = 'test';
38
+ }
39
+
40
+ container.set({ type: TestService });
41
+ const instance = container.get(TestService) as TestService;
42
+
43
+ expect(instance).toBeInstanceOf(TestService);
44
+ expect(instance.name).toBe('test');
45
+ });
46
+
47
+ it('should register and retrieve service by string identifier', () => {
48
+ class TestService {
49
+ public name = 'test';
50
+ }
51
+
52
+ container.set({ id: 'test-service', type: TestService });
53
+ const instance = container.get('test-service') as TestService;
54
+
55
+ expect(instance).toBeInstanceOf(TestService);
56
+ expect(instance.name).toBe('test');
57
+ });
58
+
59
+ it('should register and retrieve service by token', () => {
60
+ const token = new Token<TestService>('test-token');
61
+
62
+ class TestService {
63
+ public name = 'test';
64
+ }
65
+
66
+ container.set({ id: token, type: TestService });
67
+ const instance = container.get(token);
68
+
69
+ expect(instance).toBeInstanceOf(TestService);
70
+ expect(instance.name).toBe('test');
71
+ });
72
+
73
+ it('should register and retrieve service with value', () => {
74
+ const value = { name: 'test-value' };
75
+
76
+ container.set({ id: 'test-value', value });
77
+ const instance = container.get('test-value');
78
+
79
+ expect(instance).toBe(value);
80
+ });
81
+
82
+ it('should register and retrieve service with factory', () => {
83
+ const factory = () => ({ name: 'factory-created' });
84
+
85
+ container.set({ id: 'test-factory', factory });
86
+ const instance = container.get('test-factory');
87
+
88
+ expect(instance).toEqual({ name: 'factory-created' });
89
+ });
90
+
91
+ it('should register and retrieve service with factory class', () => {
92
+ class Factory {
93
+ create() {
94
+ return { name: 'factory-class-created' };
95
+ }
96
+ }
97
+
98
+ container.set({ id: 'test-factory-class', factory: [Factory, 'create'] });
99
+ const instance = container.get('test-factory-class');
100
+
101
+ expect(instance).toEqual({ name: 'factory-class-created' });
102
+ });
103
+ });
104
+
105
+ describe('Service Scopes', () => {
106
+ it('should create new instance for transient scope', () => {
107
+ class TestService {
108
+ public id = Math.random();
109
+ }
110
+
111
+ container.set({ type: TestService, scope: 'transient' });
112
+
113
+ const instance1 = container.get(TestService);
114
+ const instance2 = container.get(TestService);
115
+
116
+ expect(instance1).not.toBe(instance2);
117
+ expect(instance1.id).not.toBe(instance2.id);
118
+ });
119
+
120
+ it('should reuse instance for container scope', () => {
121
+ class TestService {
122
+ public id = Math.random();
123
+ }
124
+
125
+ container.set({ type: TestService, scope: 'container' });
126
+
127
+ const instance1 = container.get(TestService);
128
+ const instance2 = container.get(TestService);
129
+
130
+ expect(instance1).toBe(instance2);
131
+ expect(instance1.id).toBe(instance2.id);
132
+ });
133
+
134
+ it('should reuse instance for singleton scope', () => {
135
+ class TestService {
136
+ public id = Math.random();
137
+ }
138
+
139
+ container.set({ type: TestService, scope: 'singleton' });
140
+
141
+ const instance1 = container.get(TestService);
142
+ const instance2 = container.get(TestService);
143
+
144
+ expect(instance1).toBe(instance2);
145
+ expect(instance1.id).toBe(instance2.id);
146
+ });
147
+ });
148
+
149
+ describe('Multiple Services', () => {
150
+ it('should register and retrieve multiple services', () => {
151
+ class TestService1 {
152
+ public name = 'service1';
153
+ }
154
+
155
+ class TestService2 {
156
+ public name = 'service2';
157
+ }
158
+
159
+ container.set({ id: 'test-services', type: TestService1, multiple: true });
160
+ container.set({ id: 'test-services', type: TestService2, multiple: true });
161
+
162
+ const services = container.getMany('test-services');
163
+
164
+ expect(services).toHaveLength(2);
165
+ expect(services[0]).toBeInstanceOf(TestService1);
166
+ expect(services[1]).toBeInstanceOf(TestService2);
167
+ });
168
+
169
+ it('should throw error when getting single service with multiple flag', () => {
170
+ class TestService {
171
+ public name = 'test';
172
+ }
173
+
174
+ container.set({ id: 'test-services', type: TestService, multiple: true });
175
+
176
+ expect(() => container.get('test-services')).toThrow(
177
+ 'Service with "test-services" identifier was not found in the container',
178
+ );
179
+ });
180
+ });
181
+
182
+ describe('Eager Services', () => {
183
+ it('should instantiate eager service immediately', () => {
184
+ let instantiated = false;
185
+
186
+ class TestService {
187
+ constructor() {
188
+ instantiated = true;
189
+ }
190
+ }
191
+
192
+ container.set({ type: TestService, eager: true });
193
+
194
+ expect(instantiated).toBe(true);
195
+ });
196
+
197
+ it('should not instantiate eager transient service', () => {
198
+ let instantiated = false;
199
+
200
+ class TestService {
201
+ constructor() {
202
+ instantiated = true;
203
+ }
204
+ }
205
+
206
+ container.set({ type: TestService, eager: true, scope: 'transient' });
207
+
208
+ expect(instantiated).toBe(false);
209
+ });
210
+ });
211
+
212
+ describe('Error Handling', () => {
213
+ it('should throw ServiceNotFoundError when service not found', () => {
214
+ expect(() => container.get('non-existent-service')).toThrow(ServiceNotFoundError);
215
+ });
216
+
217
+ it('should throw CannotInstantiateValueError when no type or factory', () => {
218
+ container.set({ id: 'invalid-service' });
219
+
220
+ expect(() => container.get('invalid-service')).toThrow(CannotInstantiateValueError);
221
+ });
222
+
223
+ it('should throw error when using disposed container', async () => {
224
+ const testContainer = new ContainerInstance(`dispose-test-${Math.random()}`);
225
+ await testContainer.dispose();
226
+
227
+ expect(() => testContainer.get('test')).toThrow('Cannot use container after it has been disposed.');
228
+ });
229
+ });
230
+
231
+ describe('Service Removal', () => {
232
+ it('should remove single service', () => {
233
+ class TestService {
234
+ public name = 'test';
235
+ }
236
+
237
+ container.set({ type: TestService });
238
+ expect(container.has(TestService)).toBe(true);
239
+
240
+ container.remove(TestService);
241
+ expect(container.has(TestService)).toBe(false);
242
+ });
243
+
244
+ it('should remove multiple services', () => {
245
+ class TestService1 {
246
+ public name = 'test1';
247
+ }
248
+
249
+ class TestService2 {
250
+ public name = 'test2';
251
+ }
252
+
253
+ container.set({ type: TestService1 });
254
+ container.set({ type: TestService2 });
255
+
256
+ container.remove([TestService1, TestService2]);
257
+
258
+ expect(container.has(TestService1)).toBe(false);
259
+ expect(container.has(TestService2)).toBe(false);
260
+ });
261
+ });
262
+
263
+ describe('Container Reset', () => {
264
+ it('should reset values but keep services', () => {
265
+ class TestService {
266
+ public id = Math.random();
267
+ }
268
+
269
+ container.set({ type: TestService });
270
+ const instance1 = container.get(TestService);
271
+
272
+ container.reset({ strategy: 'resetValue' });
273
+ const instance2 = container.get(TestService);
274
+
275
+ expect(container.has(TestService)).toBe(true);
276
+ expect(instance1).not.toBe(instance2);
277
+ });
278
+
279
+ it('should reset services completely', () => {
280
+ class TestService {
281
+ public name = 'test';
282
+ }
283
+
284
+ container.set({ type: TestService });
285
+ expect(container.has(TestService)).toBe(true);
286
+
287
+ container.reset({ strategy: 'resetServices' });
288
+ expect(container.has(TestService)).toBe(false);
289
+ });
290
+ });
291
+
292
+ describe('Service Disposal', () => {
293
+ it('should call dispose method on service when removing', () => {
294
+ let disposed = false;
295
+
296
+ class TestService {
297
+ dispose() {
298
+ disposed = true;
299
+ }
300
+ }
301
+
302
+ container.set({ type: TestService });
303
+ const instance = container.get(TestService);
304
+
305
+ container.remove(TestService);
306
+
307
+ expect(disposed).toBe(true);
308
+ });
309
+
310
+ it('should handle disposal errors gracefully', () => {
311
+ class TestService {
312
+ dispose() {
313
+ throw new Error('Disposal error');
314
+ }
315
+ }
316
+
317
+ container.set({ type: TestService });
318
+ container.get(TestService);
319
+
320
+ // Should not throw error
321
+ expect(() => container.remove(TestService)).not.toThrow();
322
+ });
323
+ });
324
+ });
@@ -0,0 +1,279 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { ContainerInstance, ContainerRegistry } from '../index';
4
+
5
+ describe('ContainerRegistry', () => {
6
+ let container1: ContainerInstance;
7
+ let container2: ContainerInstance;
8
+
9
+ beforeEach(() => {
10
+ // Clean up any existing containers
11
+ ContainerInstance.default.reset({ strategy: 'resetServices' });
12
+ });
13
+
14
+ afterEach(async () => {
15
+ // Clean up containers
16
+ try {
17
+ if (container1 && !container1['disposed']) {
18
+ await container1.dispose();
19
+ }
20
+ } catch (error) {
21
+ // Ignore errors during cleanup
22
+ }
23
+
24
+ try {
25
+ if (container2 && !container2['disposed']) {
26
+ await container2.dispose();
27
+ }
28
+ } catch (error) {
29
+ // Ignore errors during cleanup
30
+ }
31
+
32
+ ContainerInstance.default.reset({ strategy: 'resetServices' });
33
+ });
34
+
35
+ describe('Container Registration', () => {
36
+ it('should register container automatically when created', () => {
37
+ container1 = new ContainerInstance('test-container-1');
38
+
39
+ expect(ContainerRegistry.hasContainer('test-container-1')).toBe(true);
40
+ });
41
+
42
+ it('should register default container', () => {
43
+ expect(ContainerRegistry.hasContainer('default')).toBe(true);
44
+ });
45
+
46
+ it('should throw error when registering non-ContainerInstance', () => {
47
+ expect(() => {
48
+ ContainerRegistry.registerContainer({} as any);
49
+ }).toThrow('Only ContainerInstance instances can be registered.');
50
+ });
51
+
52
+ it('should throw error when registering container with same ID', () => {
53
+ container1 = new ContainerInstance('duplicate-id');
54
+
55
+ expect(() => {
56
+ container2 = new ContainerInstance('duplicate-id');
57
+ }).toThrow('Cannot register container with same ID.');
58
+ });
59
+ });
60
+
61
+ describe('Container Retrieval', () => {
62
+ it('should retrieve registered container', () => {
63
+ container1 = new ContainerInstance('retrieve-test');
64
+
65
+ const retrieved = ContainerRegistry.getContainer('retrieve-test');
66
+ expect(retrieved).toBe(container1);
67
+ expect(retrieved.id).toBe('retrieve-test');
68
+ });
69
+
70
+ it('should throw error when retrieving non-existent container', () => {
71
+ expect(() => {
72
+ ContainerRegistry.getContainer('non-existent');
73
+ }).toThrow('No container is registered with the given ID.');
74
+ });
75
+
76
+ it('should check if container exists', () => {
77
+ expect(ContainerRegistry.hasContainer('non-existent')).toBe(false);
78
+
79
+ container1 = new ContainerInstance('exists-test');
80
+ expect(ContainerRegistry.hasContainer('exists-test')).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe('Container Removal', () => {
85
+ it('should remove container from registry', async () => {
86
+ container1 = new ContainerInstance('remove-test');
87
+
88
+ expect(ContainerRegistry.hasContainer('remove-test')).toBe(true);
89
+
90
+ await ContainerRegistry.removeContainer(container1);
91
+
92
+ expect(ContainerRegistry.hasContainer('remove-test')).toBe(false);
93
+ });
94
+
95
+ it('should throw error when removing non-existent container', async () => {
96
+ container1 = new ContainerInstance('remove-test');
97
+ await ContainerRegistry.removeContainer(container1);
98
+
99
+ expect(async () => {
100
+ await ContainerRegistry.removeContainer(container1);
101
+ }).rejects.toThrow('No container is registered with the given ID.');
102
+ });
103
+
104
+ it('should dispose container when removing', async () => {
105
+ let disposed = false;
106
+
107
+ container1 = new ContainerInstance('dispose-test');
108
+ container1['disposed'] = false;
109
+
110
+ // Override dispose method to track calls
111
+ const originalDispose = container1.dispose.bind(container1);
112
+ container1.dispose = async () => {
113
+ disposed = true;
114
+ return originalDispose();
115
+ };
116
+
117
+ await ContainerRegistry.removeContainer(container1);
118
+
119
+ expect(disposed).toBe(true);
120
+ });
121
+ });
122
+
123
+ describe('Multiple Containers', () => {
124
+ it('should handle multiple containers independently', () => {
125
+ container1 = new ContainerInstance('multi-1');
126
+ container2 = new ContainerInstance('multi-2');
127
+
128
+ expect(ContainerRegistry.hasContainer('multi-1')).toBe(true);
129
+ expect(ContainerRegistry.hasContainer('multi-2')).toBe(true);
130
+
131
+ const retrieved1 = ContainerRegistry.getContainer('multi-1');
132
+ const retrieved2 = ContainerRegistry.getContainer('multi-2');
133
+
134
+ expect(retrieved1).toBe(container1);
135
+ expect(retrieved2).toBe(container2);
136
+ expect(retrieved1).not.toBe(retrieved2);
137
+ });
138
+
139
+ it('should isolate services between containers', () => {
140
+ class TestService {
141
+ public name = 'test';
142
+ }
143
+
144
+ container1 = new ContainerInstance('isolate-1');
145
+ container2 = new ContainerInstance('isolate-2');
146
+
147
+ container1.set({ type: TestService });
148
+ container2.set({ type: TestService });
149
+
150
+ const instance1 = container1.get(TestService);
151
+ const instance2 = container2.get(TestService);
152
+
153
+ expect(instance1).not.toBe(instance2);
154
+ });
155
+ });
156
+
157
+ describe('Container Inheritance', () => {
158
+ it('should inherit handlers from default container', () => {
159
+ container1 = new ContainerInstance('inherit-test');
160
+
161
+ // Default container should have handlers array
162
+ expect(Array.isArray(ContainerInstance.default['handlers'])).toBe(true);
163
+
164
+ // Child container should inherit handlers
165
+ expect(Array.isArray(container1['handlers'])).toBe(true);
166
+ });
167
+
168
+ it('should not inherit handlers when creating default container', () => {
169
+ // Default container should not inherit from itself
170
+ expect(Array.isArray(ContainerInstance.default['handlers'])).toBe(true);
171
+ });
172
+ });
173
+
174
+ describe('Container.of() Method', () => {
175
+ it('should return default container when id is default', () => {
176
+ const container = ContainerInstance.default.of('default');
177
+ expect(container).toBe(ContainerInstance.default);
178
+ });
179
+
180
+ it('should return existing container when it exists', () => {
181
+ container1 = new ContainerInstance('of-test');
182
+
183
+ const retrieved = ContainerInstance.default.of('of-test');
184
+ expect(retrieved).toBe(container1);
185
+ });
186
+
187
+ it('should create new container when it does not exist', () => {
188
+ const newContainer = ContainerInstance.default.of('new-container');
189
+
190
+ expect(newContainer).toBeInstanceOf(ContainerInstance);
191
+ expect(newContainer.id).toBe('new-container');
192
+ expect(ContainerRegistry.hasContainer('new-container')).toBe(true);
193
+ });
194
+
195
+ it('should throw error when using disposed container', async () => {
196
+ container1 = new ContainerInstance('disposed-test');
197
+ await container1.dispose();
198
+
199
+ expect(() => container1.of('test')).toThrow('Cannot use container after it has been disposed.');
200
+ });
201
+ });
202
+
203
+ describe('Container Lifecycle', () => {
204
+ it('should handle container creation and disposal cycle', async () => {
205
+ container1 = new ContainerInstance('lifecycle-test');
206
+
207
+ // Register a service
208
+ class TestService {
209
+ public name = 'test';
210
+ }
211
+
212
+ container1.set({ type: TestService });
213
+ expect(container1.has(TestService)).toBe(true);
214
+
215
+ // Remove container
216
+ await ContainerRegistry.removeContainer(container1);
217
+
218
+ expect(ContainerRegistry.hasContainer('lifecycle-test')).toBe(false);
219
+ });
220
+
221
+ it('should handle multiple containers with same service types', () => {
222
+ class TestService {
223
+ public id = Math.random();
224
+ }
225
+
226
+ container1 = new ContainerInstance('same-service-1');
227
+ container2 = new ContainerInstance('same-service-2');
228
+
229
+ container1.set({ type: TestService });
230
+ container2.set({ type: TestService });
231
+
232
+ const instance1 = container1.get(TestService);
233
+ const instance2 = container2.get(TestService);
234
+
235
+ expect(instance1).not.toBe(instance2);
236
+ expect(instance1.id).not.toBe(instance2.id);
237
+ });
238
+ });
239
+
240
+ describe('Error Handling', () => {
241
+ it('should handle container disposal errors gracefully', async () => {
242
+ container1 = new ContainerInstance('error-test');
243
+
244
+ // Override dispose to throw error
245
+ const originalDispose = container1.dispose.bind(container1);
246
+ container1.dispose = async () => {
247
+ throw new Error('Disposal error');
248
+ };
249
+
250
+ // Should still remove from registry even if disposal fails
251
+ await expect(ContainerRegistry.removeContainer(container1)).rejects.toThrow('Disposal error');
252
+ });
253
+
254
+ it('should handle invalid container IDs', () => {
255
+ expect(() => {
256
+ ContainerRegistry.getContainer('' as any);
257
+ }).toThrow('No container is registered with the given ID.');
258
+ });
259
+ });
260
+
261
+ describe('Default Container', () => {
262
+ it('should have default container available', () => {
263
+ expect(ContainerInstance.default).toBeDefined();
264
+ expect(ContainerInstance.default.id).toBe('default');
265
+ });
266
+
267
+ it('should return same default container instance', () => {
268
+ const default1 = ContainerInstance.default;
269
+ const default2 = ContainerInstance.default;
270
+
271
+ expect(default1).toBe(default2);
272
+ });
273
+
274
+ it('should register default container in registry', () => {
275
+ // Default container should be registered when first accessed
276
+ expect(ContainerInstance.default).toBeDefined();
277
+ });
278
+ });
279
+ });