@tachybase/di 1.3.52 → 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.
@@ -0,0 +1,284 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { Container, ContainerInstance, Inject, Service } from '../index';
4
+
5
+ describe('Service Scopes', () => {
6
+ let container1: ContainerInstance;
7
+ let container2: ContainerInstance;
8
+
9
+ beforeEach(() => {
10
+ container1 = new ContainerInstance(`container1-${Math.random()}`);
11
+ container2 = new ContainerInstance(`container2-${Math.random()}`);
12
+ Container.reset({ strategy: 'resetServices' });
13
+ });
14
+
15
+ afterEach(() => {
16
+ container1.reset({ strategy: 'resetServices' });
17
+ container2.reset({ strategy: 'resetServices' });
18
+ Container.reset({ strategy: 'resetServices' });
19
+ });
20
+
21
+ describe('Container Scope', () => {
22
+ it('should create separate instances in different containers', () => {
23
+ class TestService {
24
+ public id = Math.random();
25
+ }
26
+
27
+ container1.set({ type: TestService, scope: 'container' });
28
+ container2.set({ type: TestService, scope: 'container' });
29
+
30
+ const instance1 = container1.get(TestService);
31
+ const instance2 = container2.get(TestService);
32
+
33
+ expect(instance1).not.toBe(instance2);
34
+ expect(instance1.id).not.toBe(instance2.id);
35
+ });
36
+
37
+ it('should reuse instance within same container', () => {
38
+ class TestService {
39
+ public id = Math.random();
40
+ }
41
+
42
+ container1.set({ type: TestService, scope: 'container' });
43
+
44
+ const instance1 = container1.get(TestService);
45
+ const instance2 = container1.get(TestService);
46
+
47
+ expect(instance1).toBe(instance2);
48
+ expect(instance1.id).toBe(instance2.id);
49
+ });
50
+
51
+ it('should work with decorators', () => {
52
+ @Service({ scope: 'container' })
53
+ class TestService {
54
+ public id = Math.random();
55
+ }
56
+
57
+ const instance1 = Container.get(TestService);
58
+ const instance2 = Container.get(TestService);
59
+
60
+ expect(instance1).toBe(instance2);
61
+ });
62
+ });
63
+
64
+ describe('Singleton Scope', () => {
65
+ it('should share instance across all containers', () => {
66
+ class TestService {
67
+ public id = Math.random();
68
+ }
69
+
70
+ container1.set({ type: TestService, scope: 'singleton' });
71
+ container2.set({ type: TestService, scope: 'singleton' });
72
+
73
+ const instance1 = container1.get(TestService);
74
+ const instance2 = container2.get(TestService);
75
+
76
+ expect(instance1).toBe(instance2);
77
+ expect(instance1.id).toBe(instance2.id);
78
+ });
79
+
80
+ it('should register in default container when set in child container', () => {
81
+ class TestService {
82
+ public id = Math.random();
83
+ }
84
+
85
+ container1.set({ type: TestService, scope: 'singleton' });
86
+
87
+ // Should be registered in default container
88
+ expect(Container.has(TestService)).toBe(true);
89
+ expect(container1.has(TestService)).toBe(false);
90
+
91
+ const instance1 = container1.get(TestService);
92
+ const instance2 = Container.get(TestService);
93
+
94
+ expect(instance1).toBe(instance2);
95
+ });
96
+
97
+ it('should work with decorators', () => {
98
+ @Service({ scope: 'singleton' })
99
+ class TestService {
100
+ public id = Math.random();
101
+ }
102
+
103
+ const instance1 = Container.get(TestService);
104
+ const instance2 = Container.get(TestService);
105
+
106
+ expect(instance1).toBe(instance2);
107
+ });
108
+
109
+ it('should handle multiple services with singleton scope', () => {
110
+ class TestService1 {
111
+ public name = 'service1';
112
+ }
113
+
114
+ class TestService2 {
115
+ public name = 'service2';
116
+ }
117
+
118
+ container1.set({ id: 'test-services', type: TestService1, multiple: true, scope: 'singleton' });
119
+ container2.set({ id: 'test-services', type: TestService2, multiple: true, scope: 'singleton' });
120
+
121
+ const services1 = container1.getMany('test-services');
122
+ const services2 = container2.getMany('test-services');
123
+
124
+ expect(services1).toHaveLength(2);
125
+ expect(services2).toHaveLength(2);
126
+ expect(services1[0]).toBe(services2[0]);
127
+ expect(services1[1]).toBe(services2[1]);
128
+ });
129
+ });
130
+
131
+ describe('Transient Scope', () => {
132
+ it('should create new instance every time', () => {
133
+ class TestService {
134
+ public id = Math.random();
135
+ }
136
+
137
+ container1.set({ type: TestService, scope: 'transient' });
138
+
139
+ const instance1 = container1.get(TestService);
140
+ const instance2 = container1.get(TestService);
141
+
142
+ expect(instance1).not.toBe(instance2);
143
+ expect(instance1.id).not.toBe(instance2.id);
144
+ });
145
+
146
+ it('should create new instances in different containers', () => {
147
+ class TestService {
148
+ public id = Math.random();
149
+ }
150
+
151
+ container1.set({ type: TestService, scope: 'transient' });
152
+ container2.set({ type: TestService, scope: 'transient' });
153
+
154
+ const instance1 = container1.get(TestService);
155
+ const instance2 = container2.get(TestService);
156
+
157
+ expect(instance1).not.toBe(instance2);
158
+ expect(instance1.id).not.toBe(instance2.id);
159
+ });
160
+
161
+ it('should work with decorators', () => {
162
+ @Service({ scope: 'transient' })
163
+ class TestService {
164
+ public id = Math.random();
165
+ }
166
+
167
+ const instance1 = Container.get(TestService);
168
+ const instance2 = Container.get(TestService);
169
+
170
+ expect(instance1).not.toBe(instance2);
171
+ });
172
+
173
+ it('should not store value for transient services', () => {
174
+ class TestService {
175
+ public id = Math.random();
176
+ }
177
+
178
+ container1.set({ type: TestService, scope: 'transient' });
179
+
180
+ const instance1 = container1.get(TestService);
181
+ const instance2 = container1.get(TestService);
182
+
183
+ // Both should be new instances
184
+ expect(instance1).not.toBe(instance2);
185
+ });
186
+ });
187
+
188
+ describe('Mixed Scopes', () => {
189
+ it('should handle inheritance with different scopes', () => {
190
+ @Service({ scope: 'singleton' })
191
+ class BaseService {
192
+ public id = Math.random();
193
+ }
194
+
195
+ @Service({ scope: 'transient' })
196
+ class ExtendedService extends BaseService {
197
+ public name = 'extended';
198
+ }
199
+
200
+ const instance1 = Container.get(ExtendedService);
201
+ const instance2 = Container.get(ExtendedService);
202
+
203
+ // Extended service should be transient
204
+ expect(instance1).not.toBe(instance2);
205
+
206
+ // Base service should also be transient since ExtendedService is transient
207
+ expect(instance1.id).not.toBe(instance2.id);
208
+ });
209
+ });
210
+
211
+ describe('Scope Edge Cases', () => {
212
+ it('should handle scope change after registration', () => {
213
+ class TestService {
214
+ public id = Math.random();
215
+ }
216
+
217
+ container1.set({ type: TestService, scope: 'container' });
218
+ const instance1 = container1.get(TestService);
219
+
220
+ // Change scope
221
+ container1.set({ type: TestService, scope: 'transient' });
222
+ const instance2 = container1.get(TestService);
223
+
224
+ // Should create new instance due to scope change
225
+ expect(instance1).not.toBe(instance2);
226
+ });
227
+
228
+ it('should handle eager services with different scopes', () => {
229
+ let singletonInstantiated = false;
230
+ let containerInstantiated = false;
231
+ let transientInstantiated = false;
232
+
233
+ @Service({ scope: 'singleton', eager: true })
234
+ class SingletonService {
235
+ constructor() {
236
+ singletonInstantiated = true;
237
+ }
238
+ }
239
+
240
+ @Service({ scope: 'container', eager: true })
241
+ class ContainerService {
242
+ constructor() {
243
+ containerInstantiated = true;
244
+ }
245
+ }
246
+
247
+ @Service({ scope: 'transient', eager: true })
248
+ class TransientService {
249
+ constructor() {
250
+ transientInstantiated = true;
251
+ }
252
+ }
253
+
254
+ expect(singletonInstantiated).toBe(true);
255
+ expect(containerInstantiated).toBe(true);
256
+ expect(transientInstantiated).toBe(false); // Transient eager services are ignored
257
+ });
258
+
259
+ it('should handle multiple services with different scopes', () => {
260
+ class Service1 {
261
+ public name = 'service1';
262
+ }
263
+
264
+ class Service2 {
265
+ public name = 'service2';
266
+ }
267
+
268
+ container1.set({ id: 'mixed-services', type: Service1, multiple: true, scope: 'singleton' });
269
+ container1.set({ id: 'mixed-services', type: Service2, multiple: true, scope: 'container' });
270
+
271
+ const services1 = container1.getMany('mixed-services');
272
+ const services2 = container1.getMany('mixed-services');
273
+
274
+ expect(services1).toHaveLength(1);
275
+ expect(services2).toHaveLength(1);
276
+
277
+ // First service is singleton, should be the same
278
+ expect(services1[0]).toBe(services2[0]);
279
+
280
+ // Second service is container scope, should be the same
281
+ expect(services1[1]).toBe(services2[1]);
282
+ });
283
+ });
284
+ });
@@ -0,0 +1,407 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { Container, ContainerInstance, Inject, Service, Token } from '../index';
4
+
5
+ describe('Token and Factory Patterns', () => {
6
+ let container: ContainerInstance;
7
+
8
+ beforeEach(() => {
9
+ container = new ContainerInstance(`test-container-${Math.random()}`);
10
+ Container.reset({ strategy: 'resetServices' });
11
+ });
12
+
13
+ afterEach(() => {
14
+ container.reset({ strategy: 'resetServices' });
15
+ Container.reset({ strategy: 'resetServices' });
16
+ });
17
+
18
+ describe('Token Class', () => {
19
+ it('should create token with name', () => {
20
+ const token = new Token('test-token');
21
+ expect(token.name).toBe('test-token');
22
+ });
23
+
24
+ it('should create token without name', () => {
25
+ const token = new Token();
26
+ expect(token.name).toBeUndefined();
27
+ });
28
+
29
+ it('should create token with empty string name', () => {
30
+ const token = new Token('');
31
+ expect(token.name).toBe('');
32
+ });
33
+
34
+ it('should be unique instances', () => {
35
+ const token1 = new Token('same-name');
36
+ const token2 = new Token('same-name');
37
+
38
+ expect(token1).not.toBe(token2);
39
+ expect(token1.name).toBe(token2.name);
40
+ });
41
+
42
+ it('should work as service identifier', () => {
43
+ const token = new Token('service-token');
44
+
45
+ class TestService {
46
+ public name = 'test';
47
+ }
48
+
49
+ container.set({ id: token, type: TestService });
50
+
51
+ expect(container.has(token)).toBe(true);
52
+ const instance = container.get(token);
53
+ expect(instance).toBeInstanceOf(TestService);
54
+ });
55
+
56
+ it('should work with decorators', () => {
57
+ const token = new Token('decorator-token');
58
+
59
+ @Service({ id: token })
60
+ class TestService {
61
+ public name = 'test';
62
+ }
63
+
64
+ @Service()
65
+ class ConsumerService {
66
+ @Inject(token)
67
+ public testService!: TestService;
68
+ }
69
+
70
+ const consumer = Container.get(ConsumerService);
71
+ expect(consumer.testService).toBeInstanceOf(TestService);
72
+ });
73
+
74
+ it('should work with multiple services', () => {
75
+ const token = new Token('multi-token');
76
+
77
+ class Service1 {
78
+ public name = 'service1';
79
+ }
80
+
81
+ class Service2 {
82
+ public name = 'service2';
83
+ }
84
+
85
+ container.set({ id: token, type: Service1, multiple: true });
86
+ container.set({ id: token, type: Service2, multiple: true });
87
+
88
+ const services = container.getMany(token);
89
+ expect(services).toHaveLength(2);
90
+ expect(services[0]).toBeInstanceOf(Service1);
91
+ expect(services[1]).toBeInstanceOf(Service2);
92
+ });
93
+ });
94
+
95
+ describe('Factory Functions', () => {
96
+ it('should use simple factory function', () => {
97
+ const factory = () => ({ name: 'factory-created', id: Math.random() });
98
+
99
+ container.set({ id: 'factory-service', factory });
100
+
101
+ const instance1 = container.get('factory-service');
102
+ const instance2 = container.get('factory-service');
103
+
104
+ expect(instance1.name).toBe('factory-created');
105
+ expect(instance2.name).toBe('factory-created');
106
+ expect(instance1).toBe(instance2); // Same instance due to container scope
107
+ });
108
+
109
+ it('should use factory with container parameter', () => {
110
+ const factory = (container: ContainerInstance, id: any) => {
111
+ return { name: 'factory-with-container', containerId: container.id, serviceId: id };
112
+ };
113
+
114
+ container.set({ id: 'container-factory', factory });
115
+
116
+ const instance = container.get('container-factory');
117
+ expect(instance.name).toBe('factory-with-container');
118
+ expect(instance.containerId).toMatch(/^test-container-/);
119
+ expect(instance.serviceId).toBe('container-factory');
120
+ });
121
+
122
+ it('should use factory with token parameter', () => {
123
+ const token = new Token('factory-token');
124
+ const factory = (container: ContainerInstance, id: any) => {
125
+ return { name: 'factory-with-token', tokenId: id };
126
+ };
127
+
128
+ container.set({ id: token, factory });
129
+
130
+ const instance = container.get(token);
131
+ expect(instance.name).toBe('factory-with-token');
132
+ expect(instance.tokenId).toBe(token);
133
+ });
134
+
135
+ it('should work with decorators', () => {
136
+ const factory = () => ({ name: 'decorator-factory' });
137
+
138
+ @Service({ id: 'decorator-factory-service', factory })
139
+ class TestService {}
140
+
141
+ @Service()
142
+ class ConsumerService {
143
+ @Inject('decorator-factory-service')
144
+ public testService!: any;
145
+ }
146
+
147
+ const consumer = Container.get(ConsumerService);
148
+ expect(consumer.testService.name).toBe('decorator-factory');
149
+ });
150
+ });
151
+
152
+ describe('Factory Classes', () => {
153
+ it('should use factory class with method', () => {
154
+ class TestFactory {
155
+ create(container: ContainerInstance, id: any) {
156
+ return { name: 'factory-class-created', id };
157
+ }
158
+ }
159
+
160
+ container.set({ id: 'factory-class', factory: [TestFactory, 'create'] });
161
+
162
+ const instance = container.get('factory-class');
163
+ expect(instance.name).toBe('factory-class-created');
164
+ expect(instance.id).toBe('factory-class');
165
+ });
166
+
167
+ it('should use factory class from container', () => {
168
+ class TestFactory {
169
+ create(container: ContainerInstance, id: any) {
170
+ return { name: 'factory-from-container', id };
171
+ }
172
+ }
173
+
174
+ // Register factory in container
175
+ container.set({ type: TestFactory });
176
+ container.set({ id: 'factory-from-container', factory: [TestFactory, 'create'] });
177
+
178
+ const instance = container.get('factory-from-container');
179
+ expect(instance.name).toBe('factory-from-container');
180
+ });
181
+
182
+ it('should handle factory class not in container', () => {
183
+ class TestFactory {
184
+ create(container: ContainerInstance, id: any) {
185
+ return { name: 'factory-not-in-container', id };
186
+ }
187
+ }
188
+
189
+ // Don't register factory in container
190
+ container.set({ id: 'factory-not-in-container', factory: [TestFactory, 'create'] });
191
+
192
+ const instance = container.get('factory-not-in-container');
193
+ expect(instance.name).toBe('factory-not-in-container');
194
+ });
195
+
196
+ it('should work with decorators', () => {
197
+ class TestFactory {
198
+ create() {
199
+ return { name: 'decorator-factory-class' };
200
+ }
201
+ }
202
+
203
+ @Service({ id: 'decorator-factory-class-service', factory: [TestFactory, 'create'] })
204
+ class TestService {}
205
+
206
+ @Service()
207
+ class ConsumerService {
208
+ @Inject('decorator-factory-class-service')
209
+ public testService!: any;
210
+ }
211
+
212
+ const consumer = Container.get(ConsumerService);
213
+ expect(consumer.testService.name).toBe('decorator-factory-class');
214
+ });
215
+ });
216
+
217
+ describe('Complex Factory Scenarios', () => {
218
+ it('should handle factory with dependencies', () => {
219
+ class DependencyService {
220
+ public name = 'dependency';
221
+ }
222
+
223
+ class TestFactory {
224
+ create(container: ContainerInstance, id: any) {
225
+ const dependency = container.get(DependencyService);
226
+ return { name: 'factory-with-dependency', dependency: dependency.name };
227
+ }
228
+ }
229
+
230
+ container.set({ type: DependencyService });
231
+ container.set({ id: 'factory-with-dependency', factory: [TestFactory, 'create'] });
232
+
233
+ const instance = container.get('factory-with-dependency');
234
+ expect(instance.name).toBe('factory-with-dependency');
235
+ expect(instance.dependency).toBe('dependency');
236
+ });
237
+
238
+ it('should handle factory with multiple dependencies', () => {
239
+ class Service1 {
240
+ public name = 'service1';
241
+ }
242
+
243
+ class Service2 {
244
+ public name = 'service2';
245
+ }
246
+
247
+ class TestFactory {
248
+ create(container: ContainerInstance, id: any) {
249
+ const service1 = container.get(Service1);
250
+ const service2 = container.get(Service2);
251
+ return {
252
+ name: 'factory-with-multiple-dependencies',
253
+ services: [service1.name, service2.name],
254
+ };
255
+ }
256
+ }
257
+
258
+ container.set({ type: Service1 });
259
+ container.set({ type: Service2 });
260
+ container.set({ id: 'factory-multiple-deps', factory: [TestFactory, 'create'] });
261
+
262
+ const instance = container.get('factory-multiple-deps');
263
+ expect(instance.name).toBe('factory-with-multiple-dependencies');
264
+ expect(instance.services).toEqual(['service1', 'service2']);
265
+ });
266
+
267
+ it('should handle factory with token dependencies', () => {
268
+ const token = new Token('token-dependency');
269
+
270
+ class TestFactory {
271
+ create(container: ContainerInstance, id: any) {
272
+ const dependency = container.get(token);
273
+ return { name: 'factory-with-token-dependency', dependency: dependency.name };
274
+ }
275
+ }
276
+
277
+ container.set({ id: token, value: { name: 'token-dependency-value' } });
278
+ container.set({ id: 'factory-token-deps', factory: [TestFactory, 'create'] });
279
+
280
+ const instance = container.get('factory-token-deps');
281
+ expect(instance.name).toBe('factory-with-token-dependency');
282
+ expect(instance.dependency).toBe('token-dependency-value');
283
+ });
284
+ });
285
+
286
+ describe('Factory with Different Scopes', () => {
287
+ it('should handle factory with transient scope', () => {
288
+ const factory = () => ({ id: Math.random() });
289
+
290
+ container.set({ id: 'transient-factory', factory, scope: 'transient' });
291
+
292
+ const instance1 = container.get('transient-factory');
293
+ const instance2 = container.get('transient-factory');
294
+
295
+ expect(instance1).not.toBe(instance2);
296
+ expect(instance1.id).not.toBe(instance2.id);
297
+ });
298
+
299
+ it('should handle factory with singleton scope', () => {
300
+ const factory = () => ({ id: Math.random() });
301
+
302
+ container.set({ id: 'singleton-factory', factory, scope: 'singleton' });
303
+
304
+ const instance1 = container.get('singleton-factory');
305
+ const instance2 = container.get('singleton-factory');
306
+
307
+ expect(instance1).toBe(instance2);
308
+ expect(instance1.id).toBe(instance2.id);
309
+ });
310
+
311
+ it('should handle factory with container scope', () => {
312
+ const factory = () => ({ id: Math.random() });
313
+
314
+ container.set({ id: 'container-factory', factory, scope: 'container' });
315
+
316
+ const instance1 = container.get('container-factory');
317
+ const instance2 = container.get('container-factory');
318
+
319
+ expect(instance1).toBe(instance2);
320
+ expect(instance1.id).toBe(instance2.id);
321
+ });
322
+ });
323
+
324
+ describe('Factory Error Handling', () => {
325
+ it('should propagate factory errors', () => {
326
+ const factory = () => {
327
+ throw new Error('Factory error');
328
+ };
329
+
330
+ container.set({ id: 'error-factory', factory });
331
+
332
+ expect(() => container.get('error-factory')).toThrow('Factory error');
333
+ });
334
+
335
+ it('should handle factory returning undefined', () => {
336
+ const factory = () => undefined;
337
+
338
+ container.set({ id: 'undefined-factory', factory });
339
+
340
+ // Factory returning undefined should not throw, but the value should be undefined
341
+ const result = container.get('undefined-factory');
342
+ expect(result).toBeUndefined();
343
+ });
344
+
345
+ it('should handle factory returning null', () => {
346
+ const factory = () => null;
347
+
348
+ container.set({ id: 'null-factory', factory });
349
+
350
+ // Factory returning null should not throw, but the value should be null
351
+ const result = container.get('null-factory');
352
+ expect(result).toBeNull();
353
+ });
354
+
355
+ it('should handle factory class method not found', () => {
356
+ class TestFactory {
357
+ // No create method
358
+ }
359
+
360
+ container.set({ id: 'missing-method-factory', factory: [TestFactory, 'create'] });
361
+
362
+ expect(() => container.get('missing-method-factory')).toThrow();
363
+ });
364
+ });
365
+
366
+ describe('Token and Factory Integration', () => {
367
+ it('should use token with factory', () => {
368
+ const token = new Token('token-factory');
369
+ const factory = () => ({ name: 'token-factory-created' });
370
+
371
+ container.set({ id: token, factory });
372
+
373
+ const instance = container.get(token);
374
+ expect(instance.name).toBe('token-factory-created');
375
+ });
376
+
377
+ it('should use token with factory class', () => {
378
+ const token = new Token('token-factory-class');
379
+
380
+ class TestFactory {
381
+ create() {
382
+ return { name: 'token-factory-class-created' };
383
+ }
384
+ }
385
+
386
+ container.set({ id: token, factory: [TestFactory, 'create'] });
387
+
388
+ const instance = container.get(token);
389
+ expect(instance.name).toBe('token-factory-class-created');
390
+ });
391
+
392
+ it('should use token with multiple services and factory', () => {
393
+ const token = new Token('token-multi-factory');
394
+
395
+ const factory1 = () => ({ name: 'factory1' });
396
+ const factory2 = () => ({ name: 'factory2' });
397
+
398
+ container.set({ id: token, factory: factory1, multiple: true });
399
+ container.set({ id: token, factory: factory2, multiple: true });
400
+
401
+ const services = container.getMany(token);
402
+ expect(services).toHaveLength(2);
403
+ expect(services[0].name).toBe('factory1');
404
+ expect(services[1].name).toBe('factory2');
405
+ });
406
+ });
407
+ });
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export type { ServiceOptions } from './interfaces/service-options.interface';
14
14
  export type { ServiceIdentifier } from './types/service-identifier.type';
15
15
 
16
16
  export { ContainerInstance, Container } from './container-instance.class';
17
+ export { ContainerRegistry } from './container-registry.class';
17
18
  export { Token } from './token.class';
18
19
 
19
20
  export default Container;