ts-ioc-container 46.7.0 → 46.8.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 +549 -0
- package/cjm/container/Container.js +3 -0
- package/cjm/container/EmptyContainer.js +3 -0
- package/cjm/registration/Registration.js +6 -0
- package/esm/container/Container.js +3 -0
- package/esm/container/EmptyContainer.js +3 -0
- package/esm/registration/Registration.js +6 -0
- package/package.json +3 -1
- package/typings/container/Container.d.ts +1 -0
- package/typings/container/EmptyContainer.d.ts +1 -0
- package/typings/container/IContainer.d.ts +1 -0
- package/typings/registration/IRegistration.d.ts +1 -0
- package/typings/registration/Registration.d.ts +2 -1
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
- [Instances](#instances)
|
|
33
33
|
- [Dispose](#dispose)
|
|
34
34
|
- [Lazy](#lazy) `lazy`
|
|
35
|
+
- [Lazy with registerPipe](#lazy-with-registerpipe) `lazy()`
|
|
35
36
|
- [Injector](#injector)
|
|
36
37
|
- [Metadata](#metadata) `@inject`
|
|
37
38
|
- [Simple](#simple)
|
|
@@ -533,6 +534,107 @@ describe('Instances', function () {
|
|
|
533
534
|
|
|
534
535
|
```
|
|
535
536
|
|
|
537
|
+
### Check Registration
|
|
538
|
+
Sometimes you want to check if a registration with a specific key exists in the container. This is useful for conditional registration logic, validation, and debugging.
|
|
539
|
+
|
|
540
|
+
- `hasRegistration(key)` checks if a registration exists in the current container or parent containers
|
|
541
|
+
- Checks both the current container's registrations and parent container registrations
|
|
542
|
+
- Works with string keys, symbol keys, and token keys
|
|
543
|
+
- Returns false after container disposal
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { Container, Registration as R, bindTo, register, SingleToken } from 'ts-ioc-container';
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Container Registration Checking - hasRegistration
|
|
550
|
+
*
|
|
551
|
+
* The `hasRegistration` method allows you to check if a registration with a specific key
|
|
552
|
+
* exists in the current container. This is useful for conditional registration logic,
|
|
553
|
+
* validation, and debugging.
|
|
554
|
+
*
|
|
555
|
+
* Key points:
|
|
556
|
+
* - Checks only the current container's registrations (not parent containers)
|
|
557
|
+
* - Works with string keys, symbol keys, and token keys
|
|
558
|
+
* - Returns false after container disposal
|
|
559
|
+
* - Useful for conditional registration patterns
|
|
560
|
+
*/
|
|
561
|
+
describe('hasRegistration', function () {
|
|
562
|
+
const createAppContainer = () => new Container({ tags: ['application'] });
|
|
563
|
+
|
|
564
|
+
it('should return true when registration exists with string key', function () {
|
|
565
|
+
const container = createAppContainer();
|
|
566
|
+
container.addRegistration(R.fromValue('production').bindToKey('Environment'));
|
|
567
|
+
|
|
568
|
+
expect(container.hasRegistration('Environment')).toBe(true);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should return false when registration does not exist', function () {
|
|
572
|
+
const container = createAppContainer();
|
|
573
|
+
|
|
574
|
+
expect(container.hasRegistration('NonExistentService')).toBe(false);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should work with symbol keys', function () {
|
|
578
|
+
const container = createAppContainer();
|
|
579
|
+
const serviceKey = Symbol('IService');
|
|
580
|
+
container.addRegistration(R.fromValue({ name: 'Service' }).bindToKey(serviceKey));
|
|
581
|
+
|
|
582
|
+
expect(container.hasRegistration(serviceKey)).toBe(true);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should work with token keys', function () {
|
|
586
|
+
const container = createAppContainer();
|
|
587
|
+
const loggerToken = new SingleToken<{ log: (msg: string) => void }>('ILogger');
|
|
588
|
+
container.addRegistration(R.fromValue({ log: () => {} }).bindTo(loggerToken));
|
|
589
|
+
|
|
590
|
+
expect(container.hasRegistration(loggerToken.token)).toBe(true);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('should check current container and parent registrations', function () {
|
|
594
|
+
// Parent container has a registration
|
|
595
|
+
const parent = createAppContainer();
|
|
596
|
+
parent.addRegistration(R.fromValue('parent-config').bindToKey('Config'));
|
|
597
|
+
|
|
598
|
+
// Child scope does not have the registration
|
|
599
|
+
const child = parent.createScope();
|
|
600
|
+
child.addRegistration(R.fromValue('child-service').bindToKey('Service'));
|
|
601
|
+
|
|
602
|
+
// Child should see parent's registration (checks parent as well)
|
|
603
|
+
expect(child.hasRegistration('Config')).toBe(true);
|
|
604
|
+
// Child should see its own registration
|
|
605
|
+
expect(child.hasRegistration('Service')).toBe(true);
|
|
606
|
+
// Parent should see its own registration
|
|
607
|
+
expect(parent.hasRegistration('Config')).toBe(true);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('should work with class-based registrations', function () {
|
|
611
|
+
@register(bindTo('ILogger'))
|
|
612
|
+
class Logger {}
|
|
613
|
+
|
|
614
|
+
const container = createAppContainer();
|
|
615
|
+
container.addRegistration(R.fromClass(Logger));
|
|
616
|
+
|
|
617
|
+
expect(container.hasRegistration('ILogger')).toBe(true);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should be useful for conditional registration patterns', function () {
|
|
621
|
+
const container = createAppContainer();
|
|
622
|
+
|
|
623
|
+
// Register a base service
|
|
624
|
+
container.addRegistration(R.fromValue('base-service').bindToKey('BaseService'));
|
|
625
|
+
|
|
626
|
+
// Conditionally register an extension only if base exists
|
|
627
|
+
if (container.hasRegistration('BaseService')) {
|
|
628
|
+
container.addRegistration(R.fromValue('extension-service').bindToKey('ExtensionService'));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
expect(container.hasRegistration('BaseService')).toBe(true);
|
|
632
|
+
expect(container.hasRegistration('ExtensionService')).toBe(true);
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
|
|
536
638
|
### Dispose
|
|
537
639
|
Sometimes you want to dispose container and all its scopes. For example, when you want to prevent memory leaks. Or you want to ensure that nobody can use container after it was disposed.
|
|
538
640
|
|
|
@@ -766,6 +868,453 @@ describe('lazy provider', () => {
|
|
|
766
868
|
|
|
767
869
|
```
|
|
768
870
|
|
|
871
|
+
### Lazy with registerPipe
|
|
872
|
+
The `lazy()` registerPipe can be used in two ways: with the `@register` decorator or directly on the `Provider` pipe. This allows you to defer expensive service initialization until first access.
|
|
873
|
+
|
|
874
|
+
**Use cases:**
|
|
875
|
+
- Defer expensive initialization (database connections, SMTP, external APIs)
|
|
876
|
+
- Conditional features that may not be used
|
|
877
|
+
- Breaking circular dependencies
|
|
878
|
+
- Memory optimization for optional services
|
|
879
|
+
|
|
880
|
+
**Two approaches:**
|
|
881
|
+
|
|
882
|
+
1. **With @register decorator**: Use `lazy()` as a registerPipe in the decorator
|
|
883
|
+
2. **With Provider pipe**: Use `Provider.fromClass().pipe(lazy())` directly
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
import 'reflect-metadata';
|
|
887
|
+
import { bindTo, Container, inject, lazy, Provider, register, Registration as R, singleton } from 'ts-ioc-container';
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Lazy Loading with registerPipe
|
|
891
|
+
*
|
|
892
|
+
* The lazy() registerPipe can be used in two ways:
|
|
893
|
+
* 1. With @register decorator - lazy()
|
|
894
|
+
* 2. Directly on provider - provider.lazy()
|
|
895
|
+
*
|
|
896
|
+
* Both approaches defer instantiation until first access,
|
|
897
|
+
* improving startup time and memory usage.
|
|
898
|
+
*/
|
|
899
|
+
describe('lazy registerPipe', () => {
|
|
900
|
+
// Track initialization for testing
|
|
901
|
+
const initLog: string[] = [];
|
|
902
|
+
|
|
903
|
+
beforeEach(() => {
|
|
904
|
+
initLog.length = 0;
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Example 1: Using lazy() with @register decorator
|
|
909
|
+
*
|
|
910
|
+
* The lazy() registerPipe defers service instantiation until first use.
|
|
911
|
+
* Perfect for expensive services that may not always be needed.
|
|
912
|
+
*/
|
|
913
|
+
describe('with @register decorator', () => {
|
|
914
|
+
// Database connection pool - expensive to initialize
|
|
915
|
+
@register(bindTo('DatabasePool'), singleton())
|
|
916
|
+
class DatabasePool {
|
|
917
|
+
constructor() {
|
|
918
|
+
initLog.push('DatabasePool initialized');
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
query(sql: string): string[] {
|
|
922
|
+
return [`Results for: ${sql}`];
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Analytics service - expensive, but only used occasionally
|
|
927
|
+
@register(bindTo('AnalyticsService'), lazy(), singleton())
|
|
928
|
+
class AnalyticsService {
|
|
929
|
+
constructor(@inject('DatabasePool') private db: DatabasePool) {
|
|
930
|
+
initLog.push('AnalyticsService initialized');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
trackEvent(event: string): void {
|
|
934
|
+
this.db.query(`INSERT INTO events VALUES ('${event}')`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
generateReport(): string {
|
|
938
|
+
return 'Analytics Report';
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Application service - always used
|
|
943
|
+
class AppService {
|
|
944
|
+
constructor(@inject('AnalyticsService') public analytics: AnalyticsService) {
|
|
945
|
+
initLog.push('AppService initialized');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
handleRequest(path: string): void {
|
|
949
|
+
// Most requests don't need analytics
|
|
950
|
+
if (path.includes('/admin')) {
|
|
951
|
+
// Only admin requests use analytics
|
|
952
|
+
this.analytics.trackEvent(`Admin access: ${path}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
it('should defer AnalyticsService initialization until first access', () => {
|
|
958
|
+
const container = new Container()
|
|
959
|
+
.addRegistration(R.fromClass(DatabasePool))
|
|
960
|
+
.addRegistration(R.fromClass(AnalyticsService))
|
|
961
|
+
.addRegistration(R.fromClass(AppService));
|
|
962
|
+
|
|
963
|
+
// Resolve AppService
|
|
964
|
+
const app = container.resolve<AppService>(AppService);
|
|
965
|
+
|
|
966
|
+
// AppService is initialized, but AnalyticsService is NOT (it's lazy)
|
|
967
|
+
// DatabasePool is also not initialized because AnalyticsService hasn't been accessed
|
|
968
|
+
expect(initLog).toEqual(['AppService initialized']);
|
|
969
|
+
|
|
970
|
+
// Handle non-admin request - analytics not used
|
|
971
|
+
app.handleRequest('/api/users');
|
|
972
|
+
expect(initLog).toEqual(['AppService initialized']);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it('should initialize lazy service when first accessed', () => {
|
|
976
|
+
const container = new Container()
|
|
977
|
+
.addRegistration(R.fromClass(DatabasePool))
|
|
978
|
+
.addRegistration(R.fromClass(AnalyticsService))
|
|
979
|
+
.addRegistration(R.fromClass(AppService));
|
|
980
|
+
|
|
981
|
+
const app = container.resolve<AppService>(AppService);
|
|
982
|
+
|
|
983
|
+
// Handle admin request - now analytics IS used
|
|
984
|
+
app.handleRequest('/admin/dashboard');
|
|
985
|
+
|
|
986
|
+
// AnalyticsService was initialized on first access (DatabasePool too, as a dependency)
|
|
987
|
+
expect(initLog).toEqual(['AppService initialized', 'DatabasePool initialized', 'AnalyticsService initialized']);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('should create only one instance even with multiple accesses', () => {
|
|
991
|
+
const container = new Container()
|
|
992
|
+
.addRegistration(R.fromClass(DatabasePool))
|
|
993
|
+
.addRegistration(R.fromClass(AnalyticsService))
|
|
994
|
+
.addRegistration(R.fromClass(AppService));
|
|
995
|
+
|
|
996
|
+
const app = container.resolve<AppService>(AppService);
|
|
997
|
+
|
|
998
|
+
// Access analytics multiple times
|
|
999
|
+
app.handleRequest('/admin/dashboard');
|
|
1000
|
+
app.analytics.generateReport();
|
|
1001
|
+
app.analytics.trackEvent('test');
|
|
1002
|
+
|
|
1003
|
+
// AnalyticsService initialized only once (singleton + lazy)
|
|
1004
|
+
const analyticsCount = initLog.filter((msg) => msg === 'AnalyticsService initialized').length;
|
|
1005
|
+
expect(analyticsCount).toBe(1);
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Example 2: Using lazy() directly on provider
|
|
1011
|
+
*
|
|
1012
|
+
* For manual registration, call .lazy() on the provider pipe.
|
|
1013
|
+
* This gives fine-grained control over lazy loading per dependency.
|
|
1014
|
+
*/
|
|
1015
|
+
describe('with pure provider', () => {
|
|
1016
|
+
// Email service - expensive SMTP connection
|
|
1017
|
+
class EmailService {
|
|
1018
|
+
constructor() {
|
|
1019
|
+
initLog.push('EmailService initialized - SMTP connected');
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
send(to: string, subject: string): string {
|
|
1023
|
+
return `Email sent to ${to}: ${subject}`;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// SMS service - expensive gateway connection
|
|
1028
|
+
class SmsService {
|
|
1029
|
+
constructor() {
|
|
1030
|
+
initLog.push('SmsService initialized - Gateway connected');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
send(to: string, message: string): string {
|
|
1034
|
+
return `SMS sent to ${to}: ${message}`;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Notification service - uses email and SMS, but maybe not both
|
|
1039
|
+
class NotificationService {
|
|
1040
|
+
constructor(
|
|
1041
|
+
@inject('EmailService') public email: EmailService,
|
|
1042
|
+
@inject('SmsService') public sms: SmsService,
|
|
1043
|
+
) {
|
|
1044
|
+
initLog.push('NotificationService initialized');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
notifyByEmail(user: string, message: string): string {
|
|
1048
|
+
return this.email.send(user, message);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
notifyBySms(phone: string, message: string): string {
|
|
1052
|
+
return this.sms.send(phone, message);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
it('should allow selective lazy loading - email lazy, SMS eager', () => {
|
|
1057
|
+
const container = new Container()
|
|
1058
|
+
// EmailService is lazy - won't connect to SMTP until used
|
|
1059
|
+
.addRegistration(
|
|
1060
|
+
R.fromClass(EmailService)
|
|
1061
|
+
.bindToKey('EmailService')
|
|
1062
|
+
.pipe(singleton(), (p) => p.lazy()),
|
|
1063
|
+
)
|
|
1064
|
+
// SmsService is eager - connects to gateway immediately
|
|
1065
|
+
.addRegistration(R.fromClass(SmsService).bindToKey('SmsService').pipe(singleton()))
|
|
1066
|
+
.addRegistration(R.fromClass(NotificationService));
|
|
1067
|
+
|
|
1068
|
+
// Resolve NotificationService
|
|
1069
|
+
const notifications = container.resolve<NotificationService>(NotificationService);
|
|
1070
|
+
|
|
1071
|
+
// SmsService initialized immediately (eager)
|
|
1072
|
+
// EmailService NOT initialized yet (lazy)
|
|
1073
|
+
expect(initLog).toEqual(['SmsService initialized - Gateway connected', 'NotificationService initialized']);
|
|
1074
|
+
|
|
1075
|
+
// Send SMS - already initialized
|
|
1076
|
+
notifications.notifyBySms('555-1234', 'Test');
|
|
1077
|
+
expect(initLog).toEqual(['SmsService initialized - Gateway connected', 'NotificationService initialized']);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it('should initialize lazy email service when first accessed', () => {
|
|
1081
|
+
const container = new Container()
|
|
1082
|
+
.addRegistration(
|
|
1083
|
+
R.fromClass(EmailService)
|
|
1084
|
+
.bindToKey('EmailService')
|
|
1085
|
+
.pipe(singleton(), (p) => p.lazy()),
|
|
1086
|
+
)
|
|
1087
|
+
.addRegistration(R.fromClass(SmsService).bindToKey('SmsService').pipe(singleton()))
|
|
1088
|
+
.addRegistration(R.fromClass(NotificationService));
|
|
1089
|
+
|
|
1090
|
+
const notifications = container.resolve<NotificationService>(NotificationService);
|
|
1091
|
+
|
|
1092
|
+
// Send email - NOW EmailService is initialized
|
|
1093
|
+
const result = notifications.notifyByEmail('user@example.com', 'Welcome!');
|
|
1094
|
+
|
|
1095
|
+
expect(result).toBe('Email sent to user@example.com: Welcome!');
|
|
1096
|
+
expect(initLog).toContain('EmailService initialized - SMTP connected');
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('should work with multiple lazy providers', () => {
|
|
1100
|
+
const container = new Container()
|
|
1101
|
+
// Both services are lazy
|
|
1102
|
+
.addRegistration(
|
|
1103
|
+
R.fromClass(EmailService)
|
|
1104
|
+
.bindToKey('EmailService')
|
|
1105
|
+
.pipe(singleton(), (p) => p.lazy()),
|
|
1106
|
+
)
|
|
1107
|
+
.addRegistration(
|
|
1108
|
+
R.fromClass(SmsService)
|
|
1109
|
+
.bindToKey('SmsService')
|
|
1110
|
+
.pipe(singleton(), (p) => p.lazy()),
|
|
1111
|
+
)
|
|
1112
|
+
.addRegistration(R.fromClass(NotificationService));
|
|
1113
|
+
|
|
1114
|
+
const notifications = container.resolve<NotificationService>(NotificationService);
|
|
1115
|
+
|
|
1116
|
+
// Neither service initialized yet
|
|
1117
|
+
expect(initLog).toEqual(['NotificationService initialized']);
|
|
1118
|
+
|
|
1119
|
+
// Use SMS - only SMS initialized
|
|
1120
|
+
notifications.notifyBySms('555-1234', 'Test');
|
|
1121
|
+
expect(initLog).toEqual(['NotificationService initialized', 'SmsService initialized - Gateway connected']);
|
|
1122
|
+
|
|
1123
|
+
// Use Email - now Email initialized
|
|
1124
|
+
notifications.notifyByEmail('user@example.com', 'Test');
|
|
1125
|
+
expect(initLog).toEqual([
|
|
1126
|
+
'NotificationService initialized',
|
|
1127
|
+
'SmsService initialized - Gateway connected',
|
|
1128
|
+
'EmailService initialized - SMTP connected',
|
|
1129
|
+
]);
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Example 3: Pure Provider usage (without Registration)
|
|
1135
|
+
*
|
|
1136
|
+
* Use Provider.fromClass() directly with lazy() for maximum flexibility.
|
|
1137
|
+
*/
|
|
1138
|
+
describe('with pure Provider', () => {
|
|
1139
|
+
class CacheService {
|
|
1140
|
+
constructor() {
|
|
1141
|
+
initLog.push('CacheService initialized - Redis connected');
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
get(key: string): string | null {
|
|
1145
|
+
return `cached:${key}`;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
class ApiService {
|
|
1150
|
+
constructor(@inject('CacheService') private cache: CacheService) {
|
|
1151
|
+
initLog.push('ApiService initialized');
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
fetchData(id: string): string {
|
|
1155
|
+
const cached = this.cache.get(id);
|
|
1156
|
+
return cached || `fresh:${id}`;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
it('should use Provider.fromClass with lazy() helper', () => {
|
|
1161
|
+
// Create pure provider with lazy loading
|
|
1162
|
+
const cacheProvider = Provider.fromClass(CacheService).pipe(lazy(), singleton());
|
|
1163
|
+
|
|
1164
|
+
const container = new Container();
|
|
1165
|
+
container.register('CacheService', cacheProvider);
|
|
1166
|
+
container.addRegistration(R.fromClass(ApiService));
|
|
1167
|
+
|
|
1168
|
+
const api = container.resolve<ApiService>(ApiService);
|
|
1169
|
+
|
|
1170
|
+
// CacheService not initialized yet (lazy)
|
|
1171
|
+
expect(initLog).toEqual(['ApiService initialized']);
|
|
1172
|
+
|
|
1173
|
+
// Access cache - NOW it's initialized
|
|
1174
|
+
api.fetchData('user:1');
|
|
1175
|
+
expect(initLog).toContain('CacheService initialized - Redis connected');
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it('should allow importing lazy as named export', () => {
|
|
1179
|
+
// Demonstrate that lazy() is imported from the library
|
|
1180
|
+
const cacheProvider = Provider.fromClass(CacheService).pipe(lazy());
|
|
1181
|
+
|
|
1182
|
+
const container = new Container();
|
|
1183
|
+
container.register('CacheService', cacheProvider);
|
|
1184
|
+
|
|
1185
|
+
const cache = container.resolve<CacheService>('CacheService');
|
|
1186
|
+
|
|
1187
|
+
// Not initialized until accessed
|
|
1188
|
+
expect(initLog).toEqual([]);
|
|
1189
|
+
cache.get('test');
|
|
1190
|
+
expect(initLog).toEqual(['CacheService initialized - Redis connected']);
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Example 4: Combining lazy with other pipes
|
|
1196
|
+
*
|
|
1197
|
+
* lazy() works seamlessly with other provider transformations.
|
|
1198
|
+
*/
|
|
1199
|
+
describe('combining with other pipes', () => {
|
|
1200
|
+
class ConfigService {
|
|
1201
|
+
constructor(
|
|
1202
|
+
public apiUrl: string,
|
|
1203
|
+
public timeout: number,
|
|
1204
|
+
) {
|
|
1205
|
+
initLog.push(`ConfigService initialized with ${apiUrl}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
it('should combine lazy with args and singleton', () => {
|
|
1210
|
+
const container = new Container().addRegistration(
|
|
1211
|
+
R.fromClass(ConfigService)
|
|
1212
|
+
.bindToKey('Config')
|
|
1213
|
+
.pipe(
|
|
1214
|
+
(p) => p.setArgs(() => ['https://api.example.com', 5000]),
|
|
1215
|
+
(p) => p.lazy(),
|
|
1216
|
+
)
|
|
1217
|
+
.pipe(singleton()),
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
// Config not initialized yet
|
|
1221
|
+
expect(initLog).toEqual([]);
|
|
1222
|
+
|
|
1223
|
+
// Resolve - still not initialized (lazy)
|
|
1224
|
+
const config1 = container.resolve<ConfigService>('Config');
|
|
1225
|
+
expect(initLog).toEqual([]);
|
|
1226
|
+
|
|
1227
|
+
// Access property - NOW initialized
|
|
1228
|
+
const url = config1.apiUrl;
|
|
1229
|
+
expect(url).toBe('https://api.example.com');
|
|
1230
|
+
expect(initLog).toEqual(['ConfigService initialized with https://api.example.com']);
|
|
1231
|
+
|
|
1232
|
+
// Resolve again - same instance (singleton)
|
|
1233
|
+
const config2 = container.resolve<ConfigService>('Config');
|
|
1234
|
+
expect(config2).toBe(config1);
|
|
1235
|
+
expect(initLog.length).toBe(1); // Still only one initialization
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Example 5: Real-world use case - Resource Management
|
|
1241
|
+
*
|
|
1242
|
+
* Lazy loading is ideal for:
|
|
1243
|
+
* - Database connections
|
|
1244
|
+
* - File handles
|
|
1245
|
+
* - External API clients
|
|
1246
|
+
* - Report generators
|
|
1247
|
+
*/
|
|
1248
|
+
describe('real-world example - feature flags', () => {
|
|
1249
|
+
class FeatureFlagService {
|
|
1250
|
+
constructor() {
|
|
1251
|
+
initLog.push('FeatureFlagService initialized');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
isEnabled(feature: string): boolean {
|
|
1255
|
+
return feature === 'premium';
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
@register(bindTo('PremiumFeature'), lazy(), singleton())
|
|
1260
|
+
class PremiumFeature {
|
|
1261
|
+
constructor() {
|
|
1262
|
+
initLog.push('PremiumFeature initialized - expensive operation');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
execute(): string {
|
|
1266
|
+
return 'Premium feature executed';
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
class Application {
|
|
1271
|
+
constructor(
|
|
1272
|
+
@inject('FeatureFlagService') private flags: FeatureFlagService,
|
|
1273
|
+
@inject('PremiumFeature') private premium: PremiumFeature,
|
|
1274
|
+
) {
|
|
1275
|
+
initLog.push('Application initialized');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
handleRequest(feature: string): string {
|
|
1279
|
+
if (this.flags.isEnabled(feature)) {
|
|
1280
|
+
return this.premium.execute();
|
|
1281
|
+
}
|
|
1282
|
+
return 'Standard feature';
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
it('should not initialize premium features for standard users', () => {
|
|
1287
|
+
const container = new Container()
|
|
1288
|
+
.addRegistration(R.fromClass(FeatureFlagService).bindToKey('FeatureFlagService').pipe(singleton()))
|
|
1289
|
+
.addRegistration(R.fromClass(PremiumFeature))
|
|
1290
|
+
.addRegistration(R.fromClass(Application));
|
|
1291
|
+
|
|
1292
|
+
const app = container.resolve<Application>(Application);
|
|
1293
|
+
|
|
1294
|
+
// Standard request - premium feature not initialized
|
|
1295
|
+
const result = app.handleRequest('standard');
|
|
1296
|
+
expect(result).toBe('Standard feature');
|
|
1297
|
+
expect(initLog).not.toContain('PremiumFeature initialized - expensive operation');
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it('should initialize premium features only for premium users', () => {
|
|
1301
|
+
const container = new Container()
|
|
1302
|
+
.addRegistration(R.fromClass(FeatureFlagService).bindToKey('FeatureFlagService').pipe(singleton()))
|
|
1303
|
+
.addRegistration(R.fromClass(PremiumFeature))
|
|
1304
|
+
.addRegistration(R.fromClass(Application));
|
|
1305
|
+
|
|
1306
|
+
const app = container.resolve<Application>(Application);
|
|
1307
|
+
|
|
1308
|
+
// Premium request - NOW premium feature is initialized
|
|
1309
|
+
const result = app.handleRequest('premium');
|
|
1310
|
+
expect(result).toBe('Premium feature executed');
|
|
1311
|
+
expect(initLog).toContain('PremiumFeature initialized - expensive operation');
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
```
|
|
1317
|
+
|
|
769
1318
|
## Injector
|
|
770
1319
|
`IInjector` is used to describe how dependencies should be injected to constructor.
|
|
771
1320
|
|
|
@@ -107,6 +107,9 @@ class Container {
|
|
|
107
107
|
getRegistrations() {
|
|
108
108
|
return [...this.parent.getRegistrations(), ...this.registrations];
|
|
109
109
|
}
|
|
110
|
+
hasRegistration(key) {
|
|
111
|
+
return this.registrations.some((r) => r.getKeyOrFail() === key) || this.parent.hasRegistration(key);
|
|
112
|
+
}
|
|
110
113
|
addOnConstructHook(...hooks) {
|
|
111
114
|
this.onConstructHookList.push(...hooks);
|
|
112
115
|
return this;
|
|
@@ -79,5 +79,11 @@ class Registration {
|
|
|
79
79
|
const provider = this.createProvider();
|
|
80
80
|
container.register(this.key, provider.pipe(...this.mappers), { aliases: [...this.aliases] });
|
|
81
81
|
}
|
|
82
|
+
getKeyOrFail() {
|
|
83
|
+
if (!this.key) {
|
|
84
|
+
throw new DependencyMissingKeyError_1.DependencyMissingKeyError('No key provided for registration');
|
|
85
|
+
}
|
|
86
|
+
return this.key;
|
|
87
|
+
}
|
|
82
88
|
}
|
|
83
89
|
exports.Registration = Registration;
|
|
@@ -104,6 +104,9 @@ export class Container {
|
|
|
104
104
|
getRegistrations() {
|
|
105
105
|
return [...this.parent.getRegistrations(), ...this.registrations];
|
|
106
106
|
}
|
|
107
|
+
hasRegistration(key) {
|
|
108
|
+
return this.registrations.some((r) => r.getKeyOrFail() === key) || this.parent.hasRegistration(key);
|
|
109
|
+
}
|
|
107
110
|
addOnConstructHook(...hooks) {
|
|
108
111
|
this.onConstructHookList.push(...hooks);
|
|
109
112
|
return this;
|
|
@@ -76,4 +76,10 @@ export class Registration {
|
|
|
76
76
|
const provider = this.createProvider();
|
|
77
77
|
container.register(this.key, provider.pipe(...this.mappers), { aliases: [...this.aliases] });
|
|
78
78
|
}
|
|
79
|
+
getKeyOrFail() {
|
|
80
|
+
if (!this.key) {
|
|
81
|
+
throw new DependencyMissingKeyError('No key provided for registration');
|
|
82
|
+
}
|
|
83
|
+
return this.key;
|
|
84
|
+
}
|
|
79
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-ioc-container",
|
|
3
|
-
"version": "46.
|
|
3
|
+
"version": "46.8.0",
|
|
4
4
|
"description": "Typescript IoC container",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"docs"
|
|
@@ -63,6 +63,8 @@
|
|
|
63
63
|
"docs:preview": "pnpm --filter ts-ioc-container-docs run preview"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
+
"@commitlint/cli": "^20.2.0",
|
|
67
|
+
"@commitlint/config-conventional": "^20.2.0",
|
|
66
68
|
"@semantic-release/changelog": "^6.0.3",
|
|
67
69
|
"@semantic-release/git": "^10.0.1",
|
|
68
70
|
"@semantic-release/github": "^12.0.2",
|
|
@@ -30,6 +30,7 @@ export declare class Container implements IContainer {
|
|
|
30
30
|
dispose(): void;
|
|
31
31
|
addRegistration(registration: IRegistration): this;
|
|
32
32
|
getRegistrations(): IRegistration[];
|
|
33
|
+
hasRegistration(key: DependencyKey): boolean;
|
|
33
34
|
addOnConstructHook(...hooks: OnConstructHook[]): this;
|
|
34
35
|
addOnDisposeHook(...hooks: OnDisposeHook[]): this;
|
|
35
36
|
addInstance(instance: Instance): void;
|
|
@@ -16,6 +16,7 @@ export declare class EmptyContainer implements IContainer {
|
|
|
16
16
|
hasTag(tag: Tag): boolean;
|
|
17
17
|
addTags(...tags: Tag[]): void;
|
|
18
18
|
getRegistrations(): never[];
|
|
19
|
+
hasRegistration(key: DependencyKey): boolean;
|
|
19
20
|
removeScope(): void;
|
|
20
21
|
useModule(module: IContainerModule): this;
|
|
21
22
|
addRegistration(registration: IRegistration): this;
|
|
@@ -38,6 +38,7 @@ export interface IContainer extends Tagged {
|
|
|
38
38
|
register(key: DependencyKey, value: IProvider, options?: RegisterOptions): this;
|
|
39
39
|
addRegistration(registration: IRegistration): this;
|
|
40
40
|
getRegistrations(): IRegistration[];
|
|
41
|
+
hasRegistration(key: DependencyKey): boolean;
|
|
41
42
|
resolve<T>(target: constructor<T> | DependencyKey, options?: ResolveOneOptions): T;
|
|
42
43
|
resolveByAlias<T>(alias: DependencyKey, options?: ResolveManyOptions): T[];
|
|
43
44
|
resolveOneByAlias<T>(alias: DependencyKey, options?: ResolveOneOptions): T;
|
|
@@ -6,6 +6,7 @@ import { MapFn } from '../utils/fp';
|
|
|
6
6
|
import { type constructor } from '../utils/basic';
|
|
7
7
|
export type ScopeMatchRule = (s: IContainer, prev?: boolean) => boolean;
|
|
8
8
|
export interface IRegistration<T = any> extends IContainerModule {
|
|
9
|
+
getKeyOrFail(): DependencyKey;
|
|
9
10
|
when(...predicates: ScopeMatchRule[]): this;
|
|
10
11
|
bindToKey(key: DependencyKey): this;
|
|
11
12
|
bindTo(key: DependencyKey | BindToken): this;
|
|
@@ -7,7 +7,7 @@ import { type MapFn } from '../utils/fp';
|
|
|
7
7
|
import { type constructor } from '../utils/basic';
|
|
8
8
|
export declare class Registration<T = any> implements IRegistration<T> {
|
|
9
9
|
private createProvider;
|
|
10
|
-
|
|
10
|
+
key?: DependencyKey | undefined;
|
|
11
11
|
private scopeRules;
|
|
12
12
|
static fromClass<T>(Target: constructor<T>): IRegistration<any>;
|
|
13
13
|
static fromValue<T>(value: T): IRegistration<any> | Registration<T>;
|
|
@@ -23,4 +23,5 @@ export declare class Registration<T = any> implements IRegistration<T> {
|
|
|
23
23
|
bindTo(key: DependencyKey | BindToken): this;
|
|
24
24
|
private matchScope;
|
|
25
25
|
applyTo(container: IContainer): void;
|
|
26
|
+
getKeyOrFail(): DependencyKey;
|
|
26
27
|
}
|