@tehw0lf/yaft 0.0.15 → 0.0.17
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/FeatureToggle.d.ts +20 -0
- package/FeatureToggle.js +72 -0
- package/README.md +25 -3
- package/evaluate.d.ts +42 -0
- package/evaluate.js +102 -0
- package/examples/ApiServiceBooleanProvider.d.ts +11 -0
- package/examples/ApiServiceBooleanProvider.js +85 -0
- package/examples/ApiServiceFeatureProvider.d.ts +17 -0
- package/examples/ApiServiceFeatureProvider.js +54 -0
- package/examples/LocalStorageBooleanProvider.d.ts +7 -0
- package/examples/LocalStorageBooleanProvider.js +23 -0
- package/examples/LocalStorageFeatureProvider.d.ts +14 -0
- package/examples/LocalStorageFeatureProvider.js +31 -0
- package/index.d.ts +3 -0
- package/index.js +14 -0
- package/mapping.d.ts +35 -0
- package/mapping.js +98 -0
- package/package.json +21 -9
- package/.github/workflows/build.yml +0 -28
- package/.github/workflows/security-scan.yml +0 -79
- package/CLAUDE.md +0 -151
- package/LICENSE +0 -21
- package/conformance.lock +0 -2
- package/jest.config.js +0 -16
- package/scripts/fetch-conformance.sh +0 -67
- package/src/FeatureToggle.ts +0 -95
- package/src/evaluate.ts +0 -122
- package/src/examples/ApiServiceBooleanProvider.ts +0 -82
- package/src/examples/ApiServiceFeatureProvider.ts +0 -52
- package/src/examples/LocalStorageBooleanProvider.ts +0 -25
- package/src/examples/LocalStorageFeatureProvider.ts +0 -31
- package/src/index.ts +0 -8
- package/src/mapping.ts +0 -92
- package/src/test/conformance-adapter/cases.ts +0 -104
- package/src/test/conformance-adapter/decorator.spec.ts +0 -376
- package/src/test/conformance-adapter/evaluation.spec.ts +0 -31
- package/src/test/conformance-adapter/mapping.spec.ts +0 -79
- package/src/test/decorator-behavior.spec.ts +0 -359
- package/src/test/error-handling.spec.ts +0 -396
- package/src/test/evaluate.spec.ts +0 -260
- package/src/test/feature-data.spec.ts +0 -102
- package/src/test/injectable-clock.spec.ts +0 -112
- package/src/test/integration.spec.ts +0 -472
- package/src/test/providers.spec.ts +0 -218
- package/src/test/test-boolean.json +0 -1
- package/src/test/test-feature.json +0 -20
- package/src/test/test-setup.ts +0 -2
- package/src/test/time-based-logic.spec.ts +0 -169
- package/src/test/yaft.demo.spec.ts +0 -53
- package/tsconfig.json +0 -14
- package/tsconfig.spec.json +0 -10
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { Feature } from '../FeatureToggle';
|
|
2
|
-
|
|
3
|
-
describe('Feature Data Type', () => {
|
|
4
|
-
describe('Feature structure validation', () => {
|
|
5
|
-
it('should accept valid Feature objects', () => {
|
|
6
|
-
const validFeature: Feature = {
|
|
7
|
-
key: 'testFeature',
|
|
8
|
-
value: 'true',
|
|
9
|
-
activeAt: '2023-01-01T00:00:00Z',
|
|
10
|
-
disabledAt: '2023-12-31T23:59:59Z'
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
expect(validFeature.key).toBe('testFeature');
|
|
14
|
-
expect(validFeature.value).toBe('true');
|
|
15
|
-
expect(validFeature.activeAt).toBe('2023-01-01T00:00:00Z');
|
|
16
|
-
expect(validFeature.disabledAt).toBe('2023-12-31T23:59:59Z');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should accept Feature with empty date strings', () => {
|
|
20
|
-
const featureWithEmptyDates: Feature = {
|
|
21
|
-
key: 'testFeature',
|
|
22
|
-
value: 'false',
|
|
23
|
-
activeAt: '',
|
|
24
|
-
disabledAt: ''
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
expect(featureWithEmptyDates.activeAt).toBe('');
|
|
28
|
-
expect(featureWithEmptyDates.disabledAt).toBe('');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should accept boolean-like string values', () => {
|
|
32
|
-
const trueBooleanFeature: Feature = {
|
|
33
|
-
key: 'trueFeature',
|
|
34
|
-
value: 'true',
|
|
35
|
-
activeAt: '',
|
|
36
|
-
disabledAt: ''
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const falseBooleanFeature: Feature = {
|
|
40
|
-
key: 'falseFeature',
|
|
41
|
-
value: 'false',
|
|
42
|
-
activeAt: '',
|
|
43
|
-
disabledAt: ''
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
expect(JSON.parse(trueBooleanFeature.value)).toBe(true);
|
|
47
|
-
expect(JSON.parse(falseBooleanFeature.value)).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should handle various date formats', () => {
|
|
51
|
-
const isoDateFeature: Feature = {
|
|
52
|
-
key: 'isoFeature',
|
|
53
|
-
value: 'true',
|
|
54
|
-
activeAt: '2023-01-01T00:00:00.000Z',
|
|
55
|
-
disabledAt: '2023-12-31T23:59:59.999Z'
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const simpleDateFeature: Feature = {
|
|
59
|
-
key: 'simpleFeature',
|
|
60
|
-
value: 'true',
|
|
61
|
-
activeAt: '2023-01-01',
|
|
62
|
-
disabledAt: '2023-12-31'
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
expect(Date.parse(isoDateFeature.activeAt)).not.toBeNaN();
|
|
66
|
-
expect(Date.parse(isoDateFeature.disabledAt)).not.toBeNaN();
|
|
67
|
-
expect(Date.parse(simpleDateFeature.activeAt)).not.toBeNaN();
|
|
68
|
-
expect(Date.parse(simpleDateFeature.disabledAt)).not.toBeNaN();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('Feature collection structures', () => {
|
|
73
|
-
it('should handle Record<string, Feature> collections', () => {
|
|
74
|
-
const featureCollection: Record<string, Feature> = {
|
|
75
|
-
'feature1': {
|
|
76
|
-
key: 'feature1',
|
|
77
|
-
value: 'true',
|
|
78
|
-
activeAt: '',
|
|
79
|
-
disabledAt: ''
|
|
80
|
-
},
|
|
81
|
-
'feature2': {
|
|
82
|
-
key: 'feature2',
|
|
83
|
-
value: 'false',
|
|
84
|
-
activeAt: '2023-01-01',
|
|
85
|
-
disabledAt: '2023-12-31'
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
expect(Object.keys(featureCollection)).toHaveLength(2);
|
|
90
|
-
expect(featureCollection['feature1']).toBeDefined();
|
|
91
|
-
expect(featureCollection['feature2']).toBeDefined();
|
|
92
|
-
expect(featureCollection['nonexistent']).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should handle empty feature collections', () => {
|
|
96
|
-
const emptyCollection: Record<string, Feature> = {};
|
|
97
|
-
|
|
98
|
-
expect(Object.keys(emptyCollection)).toHaveLength(0);
|
|
99
|
-
expect(emptyCollection['anyKey']).toBeUndefined();
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { Clock } from '../evaluate';
|
|
2
|
-
import { Feature } from '../FeatureToggle';
|
|
3
|
-
import { ApiServiceFeatureProvider } from '../examples/ApiServiceFeatureProvider';
|
|
4
|
-
import { LocalStorageFeatureProvider } from '../examples/LocalStorageFeatureProvider';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The clock has to be injectable for the conformance suite: every case there
|
|
8
|
-
* carries its own `now`, and an adapter cannot set it if the providers read
|
|
9
|
-
* `Date.now()` directly.
|
|
10
|
-
*
|
|
11
|
-
* These tests pin that down, and pin down that both feature-shaped providers
|
|
12
|
-
* answer identically -- the point of moving the logic into evaluate().
|
|
13
|
-
*/
|
|
14
|
-
describe('injectable clock', () => {
|
|
15
|
-
const windowed: Feature = {
|
|
16
|
-
key: 'windowed',
|
|
17
|
-
value: 'true',
|
|
18
|
-
activeAt: '2026-09-18T10:00:00Z',
|
|
19
|
-
disabledAt: '2026-09-18T14:00:00Z',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const at = (iso: string): Clock => () => Date.parse(iso);
|
|
23
|
-
|
|
24
|
-
/** A provider holding `windowed`, without touching the filesystem. */
|
|
25
|
-
const localAt = (iso: string) => {
|
|
26
|
-
const provider = new LocalStorageFeatureProvider('', at(iso));
|
|
27
|
-
provider.data = { windowed };
|
|
28
|
-
return provider;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** The same, for the API provider, without touching the network. */
|
|
32
|
-
const apiAt = (iso: string) => {
|
|
33
|
-
const provider = new ApiServiceFeatureProvider('', '', at(iso));
|
|
34
|
-
provider.data = { windowed };
|
|
35
|
-
return provider;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
describe('LocalStorageFeatureProvider', () => {
|
|
39
|
-
it('is off before the window', () => {
|
|
40
|
-
expect(localAt('2026-09-18T09:59:59Z').isEnabled('windowed')).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('is on inside the window', () => {
|
|
44
|
-
expect(localAt('2026-09-18T12:00:00Z').isEnabled('windowed')).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('is off after the window', () => {
|
|
48
|
-
expect(localAt('2026-09-18T14:00:00Z').isEnabled('windowed')).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('ApiServiceFeatureProvider', () => {
|
|
53
|
-
it('is off before the window', () => {
|
|
54
|
-
expect(apiAt('2026-09-18T09:59:59Z').isEnabled('windowed')).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('is on inside the window', () => {
|
|
58
|
-
expect(apiAt('2026-09-18T12:00:00Z').isEnabled('windowed')).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('is off after the window', () => {
|
|
62
|
-
expect(apiAt('2026-09-18T14:00:00Z').isEnabled('windowed')).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Before this change each provider carried its own copy of the logic, so
|
|
67
|
-
// they could drift apart unnoticed. Now they must agree everywhere.
|
|
68
|
-
it('both feature providers agree at every instant', () => {
|
|
69
|
-
for (const iso of [
|
|
70
|
-
'2026-09-18T09:00:00Z',
|
|
71
|
-
'2026-09-18T10:00:00Z',
|
|
72
|
-
'2026-09-18T12:00:00Z',
|
|
73
|
-
'2026-09-18T13:59:59Z',
|
|
74
|
-
'2026-09-18T14:00:00Z',
|
|
75
|
-
'2026-09-18T20:00:00Z',
|
|
76
|
-
]) {
|
|
77
|
-
expect(localAt(iso).isEnabled('windowed')).toBe(
|
|
78
|
-
apiAt(iso).isEnabled('windowed')
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('the same provider answers differently as its clock advances', () => {
|
|
84
|
-
let now = Date.parse('2026-09-18T09:00:00Z');
|
|
85
|
-
const provider = new LocalStorageFeatureProvider('', () => now);
|
|
86
|
-
provider.data = { windowed };
|
|
87
|
-
|
|
88
|
-
expect(provider.isEnabled('windowed')).toBe(false);
|
|
89
|
-
|
|
90
|
-
now = Date.parse('2026-09-18T12:00:00Z');
|
|
91
|
-
expect(provider.isEnabled('windowed')).toBe(true);
|
|
92
|
-
|
|
93
|
-
now = Date.parse('2026-09-18T15:00:00Z');
|
|
94
|
-
expect(provider.isEnabled('windowed')).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('defaults to the system clock when none is given', () => {
|
|
98
|
-
const provider = new LocalStorageFeatureProvider('');
|
|
99
|
-
provider.data = {
|
|
100
|
-
open: { key: 'open', value: 'true', activeAt: '', disabledAt: '' },
|
|
101
|
-
expired: {
|
|
102
|
-
key: 'expired',
|
|
103
|
-
value: 'true',
|
|
104
|
-
activeAt: '',
|
|
105
|
-
disabledAt: '2000-01-01T00:00:00Z',
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
expect(provider.isEnabled('open')).toBe(true);
|
|
110
|
-
expect(provider.isEnabled('expired')).toBe(false);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
import { FeatureToggle, FeatureToggleBase, Feature } from '../FeatureToggle';
|
|
2
|
-
import { LocalStorageFeatureProvider } from '../examples/LocalStorageFeatureProvider';
|
|
3
|
-
import { LocalStorageBooleanProvider } from '../examples/LocalStorageBooleanProvider';
|
|
4
|
-
|
|
5
|
-
describe('Integration Tests', () => {
|
|
6
|
-
describe('Real-world Usage Scenarios', () => {
|
|
7
|
-
describe('E-commerce Feature Toggles', () => {
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
// Mock feature configuration for e-commerce scenarios
|
|
10
|
-
const ecommerceFeatures: Record<string, Feature> = {
|
|
11
|
-
'checkout_v2': {
|
|
12
|
-
key: 'checkout_v2',
|
|
13
|
-
value: 'true',
|
|
14
|
-
activeAt: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
|
|
15
|
-
disabledAt: new Date(Date.now() + 86400000).toISOString() // 1 day from now
|
|
16
|
-
},
|
|
17
|
-
'premium_features': {
|
|
18
|
-
key: 'premium_features',
|
|
19
|
-
value: 'false',
|
|
20
|
-
activeAt: '',
|
|
21
|
-
disabledAt: ''
|
|
22
|
-
},
|
|
23
|
-
'beta_ui': {
|
|
24
|
-
key: 'beta_ui',
|
|
25
|
-
value: 'true',
|
|
26
|
-
activeAt: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
|
|
27
|
-
disabledAt: new Date(Date.now() + 86400000).toISOString() // 1 day from now
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const provider = new (class extends LocalStorageFeatureProvider {
|
|
32
|
-
constructor() {
|
|
33
|
-
super('');
|
|
34
|
-
this.data = ecommerceFeatures;
|
|
35
|
-
}
|
|
36
|
-
})();
|
|
37
|
-
|
|
38
|
-
FeatureToggleBase.featureProvider = provider;
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should handle checkout flow feature toggle', () => {
|
|
42
|
-
class LegacyCheckout {
|
|
43
|
-
process(items: string[]) {
|
|
44
|
-
return `Legacy checkout processing ${items.length} items`;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
class NewCheckout {
|
|
49
|
-
process(items: string[]) {
|
|
50
|
-
return `New checkout v2 processing ${items.length} items`;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
@FeatureToggle('checkout_v2', LegacyCheckout)
|
|
55
|
-
class CheckoutService {
|
|
56
|
-
process(items: string[]) {
|
|
57
|
-
return `Enhanced checkout processing ${items.length} items`;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const checkout = new CheckoutService();
|
|
62
|
-
const result = checkout.process(['item1', 'item2']);
|
|
63
|
-
|
|
64
|
-
expect(result).toBe('Enhanced checkout processing 2 items');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should handle premium features with method-level toggles', () => {
|
|
68
|
-
function basicRecommendations() {
|
|
69
|
-
return ['Popular item 1', 'Popular item 2'];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
class RecommendationService {
|
|
73
|
-
@FeatureToggle('premium_features', basicRecommendations)
|
|
74
|
-
getPersonalizedRecommendations(userId: string) {
|
|
75
|
-
return [`AI recommendation for ${userId}`, `Premium suggestion for ${userId}`];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
getBasicRecommendations() {
|
|
79
|
-
return ['Basic item 1', 'Basic item 2'];
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const service = new RecommendationService();
|
|
84
|
-
const recommendations = service.getPersonalizedRecommendations('user123');
|
|
85
|
-
|
|
86
|
-
expect(recommendations).toEqual(['Popular item 1', 'Popular item 2']);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should handle UI feature toggles', () => {
|
|
90
|
-
class StandardUI {
|
|
91
|
-
render() {
|
|
92
|
-
return '<div>Standard UI</div>';
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
@FeatureToggle('beta_ui', StandardUI)
|
|
97
|
-
class BetaUI {
|
|
98
|
-
render() {
|
|
99
|
-
return '<div>Beta UI with new features</div>';
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const ui = new BetaUI();
|
|
104
|
-
const rendered = ui.render();
|
|
105
|
-
|
|
106
|
-
expect(rendered).toBe('<div>Beta UI with new features</div>');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe('Microservice Feature Coordination', () => {
|
|
111
|
-
it('should handle multiple services with coordinated feature toggles', () => {
|
|
112
|
-
const serviceFeatures = {
|
|
113
|
-
'service_a_feature': true,
|
|
114
|
-
'service_b_feature': false,
|
|
115
|
-
'shared_feature': true
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const provider = new (class extends LocalStorageBooleanProvider {
|
|
119
|
-
constructor() {
|
|
120
|
-
super('');
|
|
121
|
-
this.data = serviceFeatures;
|
|
122
|
-
}
|
|
123
|
-
})();
|
|
124
|
-
|
|
125
|
-
FeatureToggleBase.featureProvider = provider;
|
|
126
|
-
|
|
127
|
-
class ServiceA {
|
|
128
|
-
@FeatureToggle('service_a_feature')
|
|
129
|
-
processData(data: any) {
|
|
130
|
-
return `ServiceA: Enhanced processing of ${JSON.stringify(data)}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
@FeatureToggle('shared_feature')
|
|
134
|
-
sharedFunction() {
|
|
135
|
-
return 'ServiceA: Shared feature active';
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
class ServiceB {
|
|
140
|
-
@FeatureToggle('service_b_feature')
|
|
141
|
-
transformData(data: any) {
|
|
142
|
-
return `ServiceB: New transformation of ${JSON.stringify(data)}`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
@FeatureToggle('shared_feature')
|
|
146
|
-
sharedFunction() {
|
|
147
|
-
return 'ServiceB: Shared feature active';
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const serviceA = new ServiceA();
|
|
152
|
-
const serviceB = new ServiceB();
|
|
153
|
-
|
|
154
|
-
expect(serviceA.processData({ test: true })).toBe('ServiceA: Enhanced processing of {"test":true}');
|
|
155
|
-
expect(serviceB.transformData({ test: true })).toBeUndefined(); // Feature disabled
|
|
156
|
-
expect(serviceA.sharedFunction()).toBe('ServiceA: Shared feature active');
|
|
157
|
-
expect(serviceB.sharedFunction()).toBe('ServiceB: Shared feature active');
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe('A/B Testing Scenarios', () => {
|
|
162
|
-
it('should support A/B testing with multiple variants', () => {
|
|
163
|
-
// This test demonstrates A/B testing where the feature evaluation
|
|
164
|
-
// happens at class definition time, not method call time
|
|
165
|
-
|
|
166
|
-
function variantA() {
|
|
167
|
-
return 'Algorithm A result';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function variantB() {
|
|
171
|
-
return 'Algorithm B result';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const abTestFeatures = {
|
|
175
|
-
'algorithm_variant_a': false,
|
|
176
|
-
'algorithm_variant_b': true
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const provider = new (class extends LocalStorageBooleanProvider {
|
|
180
|
-
constructor() {
|
|
181
|
-
super('');
|
|
182
|
-
this.data = abTestFeatures;
|
|
183
|
-
}
|
|
184
|
-
})();
|
|
185
|
-
|
|
186
|
-
// Set provider BEFORE defining the class (critical for decorator evaluation)
|
|
187
|
-
FeatureToggleBase.featureProvider = provider;
|
|
188
|
-
|
|
189
|
-
// Define class with enabled variant
|
|
190
|
-
class AlgorithmB {
|
|
191
|
-
@FeatureToggle('algorithm_variant_b', variantB)
|
|
192
|
-
calculate(input: number) {
|
|
193
|
-
return 'Default algorithm result';
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Define class with disabled variant
|
|
198
|
-
class AlgorithmA {
|
|
199
|
-
@FeatureToggle('algorithm_variant_a', variantA)
|
|
200
|
-
calculate(input: number) {
|
|
201
|
-
return 'Default algorithm result';
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const algorithmB = new AlgorithmB();
|
|
206
|
-
const algorithmA = new AlgorithmA();
|
|
207
|
-
|
|
208
|
-
const resultB = algorithmB.calculate(42);
|
|
209
|
-
const resultA = algorithmA.calculate(42);
|
|
210
|
-
|
|
211
|
-
// Enabled variant should use original method (returns "Default algorithm result")
|
|
212
|
-
// Disabled variant should use fallback method
|
|
213
|
-
expect(resultB).toBe('Default algorithm result'); // enabled, uses original
|
|
214
|
-
expect(resultA).toBe('Algorithm A result'); // disabled, uses fallback
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe('Performance and Load Testing', () => {
|
|
220
|
-
it('should handle high-frequency feature toggle evaluations', () => {
|
|
221
|
-
const provider = new (class extends LocalStorageBooleanProvider {
|
|
222
|
-
constructor() {
|
|
223
|
-
super('');
|
|
224
|
-
this.data = { 'myToggle': true };
|
|
225
|
-
}
|
|
226
|
-
})();
|
|
227
|
-
FeatureToggleBase.featureProvider = provider;
|
|
228
|
-
|
|
229
|
-
class HighFrequencyService {
|
|
230
|
-
@FeatureToggle('myToggle')
|
|
231
|
-
processItem(item: any) {
|
|
232
|
-
return `Processed: ${item}`;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const service = new HighFrequencyService();
|
|
237
|
-
const items = Array.from({ length: 1000 }, (_, i) => `item${i}`);
|
|
238
|
-
|
|
239
|
-
const start = performance.now();
|
|
240
|
-
const results = items.map(item => service.processItem(item));
|
|
241
|
-
const end = performance.now();
|
|
242
|
-
|
|
243
|
-
expect(results).toHaveLength(1000);
|
|
244
|
-
expect(results[0]).toBe('Processed: item0');
|
|
245
|
-
expect(results[999]).toBe('Processed: item999');
|
|
246
|
-
|
|
247
|
-
// Performance check - should complete within reasonable time
|
|
248
|
-
expect(end - start).toBeLessThan(100); // 100ms for 1000 operations
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('should handle concurrent access to feature toggles', async () => {
|
|
252
|
-
const provider = new (class extends LocalStorageBooleanProvider {
|
|
253
|
-
constructor() {
|
|
254
|
-
super('');
|
|
255
|
-
this.data = { 'myToggle': true };
|
|
256
|
-
}
|
|
257
|
-
})();
|
|
258
|
-
FeatureToggleBase.featureProvider = provider;
|
|
259
|
-
|
|
260
|
-
class ConcurrentService {
|
|
261
|
-
@FeatureToggle('myToggle')
|
|
262
|
-
async asyncProcess(id: number) {
|
|
263
|
-
await new Promise(resolve => setTimeout(resolve, 1));
|
|
264
|
-
return `Async result ${id}`;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const service = new ConcurrentService();
|
|
269
|
-
const promises = Array.from({ length: 50 }, (_, i) => service.asyncProcess(i));
|
|
270
|
-
|
|
271
|
-
const results = await Promise.all(promises);
|
|
272
|
-
|
|
273
|
-
expect(results).toHaveLength(50);
|
|
274
|
-
expect(results[0]).toBe('Async result 0');
|
|
275
|
-
expect(results[49]).toBe('Async result 49');
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
describe('Cross-Provider Compatibility', () => {
|
|
280
|
-
it('should work consistently across different provider types', () => {
|
|
281
|
-
// Test with Feature provider using mock data that enables myMethodToggle
|
|
282
|
-
const featureProvider = new (class extends LocalStorageFeatureProvider {
|
|
283
|
-
constructor() {
|
|
284
|
-
super('');
|
|
285
|
-
this.data = {
|
|
286
|
-
'myMethodToggle': {
|
|
287
|
-
key: 'myMethodToggle',
|
|
288
|
-
value: 'true',
|
|
289
|
-
activeAt: new Date(Date.now() - 86400000).toISOString(),
|
|
290
|
-
disabledAt: new Date(Date.now() + 86400000).toISOString()
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
})();
|
|
295
|
-
FeatureToggleBase.featureProvider = featureProvider;
|
|
296
|
-
|
|
297
|
-
class TestService {
|
|
298
|
-
@FeatureToggle('myMethodToggle')
|
|
299
|
-
testMethod() {
|
|
300
|
-
return 'feature provider result';
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
let service = new TestService();
|
|
305
|
-
let result = service.testMethod();
|
|
306
|
-
expect(result).toBe('feature provider result');
|
|
307
|
-
|
|
308
|
-
// Switch to Boolean provider
|
|
309
|
-
const booleanProvider = new (class extends LocalStorageBooleanProvider {
|
|
310
|
-
constructor() {
|
|
311
|
-
super('');
|
|
312
|
-
this.data = { 'myToggle': true };
|
|
313
|
-
}
|
|
314
|
-
})();
|
|
315
|
-
FeatureToggleBase.featureProvider = booleanProvider;
|
|
316
|
-
|
|
317
|
-
// Create new class instance to trigger re-evaluation
|
|
318
|
-
class TestService2 {
|
|
319
|
-
@FeatureToggle('myToggle')
|
|
320
|
-
testMethod() {
|
|
321
|
-
return 'boolean provider result';
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
service = new TestService2();
|
|
326
|
-
result = service.testMethod();
|
|
327
|
-
expect(result).toBe('boolean provider result');
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
describe('Real-world Integration Patterns', () => {
|
|
332
|
-
it('should support factory pattern with feature toggles', () => {
|
|
333
|
-
const factoryFeatures = {
|
|
334
|
-
'use_redis_cache': true,
|
|
335
|
-
'use_memory_cache': false
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const provider = new (class extends LocalStorageBooleanProvider {
|
|
339
|
-
constructor() {
|
|
340
|
-
super('');
|
|
341
|
-
this.data = factoryFeatures;
|
|
342
|
-
}
|
|
343
|
-
})();
|
|
344
|
-
|
|
345
|
-
FeatureToggleBase.featureProvider = provider;
|
|
346
|
-
|
|
347
|
-
class MemoryCache {
|
|
348
|
-
get(key: string) { return `memory:${key}`; }
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
class RedisCache {
|
|
352
|
-
get(key: string) { return `redis:${key}`; }
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
class CacheFactory {
|
|
356
|
-
@FeatureToggle('use_redis_cache', () => new MemoryCache())
|
|
357
|
-
createCache() {
|
|
358
|
-
return new RedisCache();
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const factory = new CacheFactory();
|
|
363
|
-
const cache = factory.createCache();
|
|
364
|
-
|
|
365
|
-
expect(cache).toBeInstanceOf(RedisCache);
|
|
366
|
-
expect(cache.get('test')).toBe('redis:test');
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('should support middleware pattern with feature toggles', () => {
|
|
370
|
-
const middlewareFeatures = {
|
|
371
|
-
'auth_middleware': true,
|
|
372
|
-
'logging_middleware': false,
|
|
373
|
-
'rate_limit_middleware': true
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
const provider = new (class extends LocalStorageBooleanProvider {
|
|
377
|
-
constructor() {
|
|
378
|
-
super('');
|
|
379
|
-
this.data = middlewareFeatures;
|
|
380
|
-
}
|
|
381
|
-
})();
|
|
382
|
-
|
|
383
|
-
FeatureToggleBase.featureProvider = provider;
|
|
384
|
-
|
|
385
|
-
class RequestProcessor {
|
|
386
|
-
private logs: string[] = [];
|
|
387
|
-
|
|
388
|
-
@FeatureToggle('auth_middleware')
|
|
389
|
-
authenticate(request: any) {
|
|
390
|
-
this.logs.push('auth executed');
|
|
391
|
-
return { ...request, authenticated: true };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
@FeatureToggle('logging_middleware')
|
|
395
|
-
log(request: any) {
|
|
396
|
-
this.logs.push('logging executed');
|
|
397
|
-
return request;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
@FeatureToggle('rate_limit_middleware')
|
|
401
|
-
rateLimit(request: any) {
|
|
402
|
-
this.logs.push('rate limit executed');
|
|
403
|
-
return request;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
process(request: any) {
|
|
407
|
-
let processedRequest = request;
|
|
408
|
-
processedRequest = this.authenticate(processedRequest) || processedRequest;
|
|
409
|
-
processedRequest = this.log(processedRequest) || processedRequest;
|
|
410
|
-
processedRequest = this.rateLimit(processedRequest) || processedRequest;
|
|
411
|
-
return processedRequest;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
getLogs() { return this.logs; }
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const processor = new RequestProcessor();
|
|
418
|
-
const result = processor.process({ id: 1 });
|
|
419
|
-
|
|
420
|
-
expect(result.authenticated).toBe(true);
|
|
421
|
-
expect(processor.getLogs()).toEqual(['auth executed', 'rate limit executed']);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('should support plugin architecture with feature toggles', () => {
|
|
425
|
-
const pluginFeatures = {
|
|
426
|
-
'analytics_plugin': false,
|
|
427
|
-
'social_sharing_plugin': true,
|
|
428
|
-
'comments_plugin': true
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
const provider = new (class extends LocalStorageBooleanProvider {
|
|
432
|
-
constructor() {
|
|
433
|
-
super('');
|
|
434
|
-
this.data = pluginFeatures;
|
|
435
|
-
}
|
|
436
|
-
})();
|
|
437
|
-
|
|
438
|
-
FeatureToggleBase.featureProvider = provider;
|
|
439
|
-
|
|
440
|
-
class PluginManager {
|
|
441
|
-
private activePlugins: string[] = [];
|
|
442
|
-
|
|
443
|
-
@FeatureToggle('analytics_plugin')
|
|
444
|
-
loadAnalytics() {
|
|
445
|
-
this.activePlugins.push('analytics');
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
@FeatureToggle('social_sharing_plugin')
|
|
449
|
-
loadSocialSharing() {
|
|
450
|
-
this.activePlugins.push('social-sharing');
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
@FeatureToggle('comments_plugin')
|
|
454
|
-
loadComments() {
|
|
455
|
-
this.activePlugins.push('comments');
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
initialize() {
|
|
459
|
-
this.loadAnalytics();
|
|
460
|
-
this.loadSocialSharing();
|
|
461
|
-
this.loadComments();
|
|
462
|
-
return this.activePlugins;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const manager = new PluginManager();
|
|
467
|
-
const activePlugins = manager.initialize();
|
|
468
|
-
|
|
469
|
-
expect(activePlugins).toEqual(['social-sharing', 'comments']);
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
});
|