@anterprize/fturex 0.0.1

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/dist/FtureXClient.d.ts +72 -0
  4. package/dist/FtureXClient.js +427 -0
  5. package/dist/angular/feature-toggle.directive.d.ts +32 -0
  6. package/dist/angular/feature-toggle.directive.js +77 -0
  7. package/dist/angular/feature-toggle.pipe.d.ts +20 -0
  8. package/dist/angular/feature-toggle.pipe.js +37 -0
  9. package/dist/angular/fturex.config.d.ts +7 -0
  10. package/dist/angular/fturex.config.js +2 -0
  11. package/dist/angular/fturex.module.d.ts +23 -0
  12. package/dist/angular/fturex.module.js +48 -0
  13. package/dist/angular/fturex.service.d.ts +31 -0
  14. package/dist/angular/fturex.service.js +56 -0
  15. package/dist/angular/index.d.ts +6 -0
  16. package/dist/angular/index.js +5 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +2 -0
  19. package/dist/opentelemetry/FtureXOtelHook.d.ts +58 -0
  20. package/dist/opentelemetry/FtureXOtelHook.js +86 -0
  21. package/dist/opentelemetry/index.d.ts +2 -0
  22. package/dist/opentelemetry/index.js +1 -0
  23. package/dist/react/FeatureToggle.d.ts +14 -0
  24. package/dist/react/FeatureToggle.js +16 -0
  25. package/dist/react/FeatureToggleProvider.d.ts +15 -0
  26. package/dist/react/FeatureToggleProvider.js +17 -0
  27. package/dist/react/index.d.ts +3 -0
  28. package/dist/react/index.js +3 -0
  29. package/dist/react/useFeatureToggle.d.ts +27 -0
  30. package/dist/react/useFeatureToggle.js +104 -0
  31. package/dist/svelte/index.d.ts +2 -0
  32. package/dist/svelte/index.js +1 -0
  33. package/dist/svelte/useFeatureToggle.d.ts +54 -0
  34. package/dist/svelte/useFeatureToggle.js +85 -0
  35. package/dist/types.d.ts +122 -0
  36. package/dist/types.js +1 -0
  37. package/dist/vue/index.d.ts +1 -0
  38. package/dist/vue/index.js +2 -0
  39. package/dist/vue/useFeatureToggle.d.ts +32 -0
  40. package/dist/vue/useFeatureToggle.js +104 -0
  41. package/package.json +99 -0
  42. package/src/vue/FeatureToggle.vue +28 -0
@@ -0,0 +1,427 @@
1
+ /**
2
+ * FtureX Client
3
+ * Fetches the full feature manifest in the background and evaluates
4
+ * conditions locally — no per-call network round-trips.
5
+ */
6
+ export class FtureXClient {
7
+ constructor(config, cacheOptions = {}) {
8
+ // Hooks
9
+ this.hooks = [];
10
+ // Manifest replaced atomically on each background refresh (JS is single-threaded)
11
+ this.manifest = new Map();
12
+ // Event listeners
13
+ this.listeners = new Map([
14
+ ["update", new Set()],
15
+ ["ready", new Set()],
16
+ ]);
17
+ // Statistics tracking
18
+ this.featureHitCounts = new Map();
19
+ this.featureEnabledCounts = new Map();
20
+ this.featureDisabledCounts = new Map();
21
+ this.featureLastAccessed = new Map();
22
+ this.config = {
23
+ baseUrl: config.baseUrl,
24
+ appKey: config.appKey,
25
+ sendStatistics: config.sendStatistics ?? true,
26
+ };
27
+ this.cacheOptions = {
28
+ refreshIntervalSeconds: cacheOptions.refreshIntervalSeconds ?? 30,
29
+ enableLocalStoragePersistence: cacheOptions.enableLocalStoragePersistence ?? true,
30
+ localStorageKey: cacheOptions.localStorageKey ?? "feature-toggle-cache",
31
+ };
32
+ }
33
+ /**
34
+ * Subscribe to an event.
35
+ * - 'ready' — fires once after the first successful manifest fetch
36
+ * - 'update' — fires after every subsequent manifest refresh
37
+ */
38
+ on(event, callback) {
39
+ this.listeners.get(event).add(callback);
40
+ }
41
+ /** Unsubscribe a previously registered callback. */
42
+ off(event, callback) {
43
+ this.listeners.get(event).delete(callback);
44
+ }
45
+ emit(event) {
46
+ this.listeners.get(event).forEach((cb) => cb());
47
+ }
48
+ /**
49
+ * Initialize the client — loads persisted manifest and starts background services
50
+ */
51
+ async initialize() {
52
+ await this.loadManifestFromStorage();
53
+ await this.refreshManifest(); // initial fetch
54
+ this.emit("ready");
55
+ this.startBackgroundRefresh();
56
+ if (this.config.sendStatistics) {
57
+ this.startStatisticsReporting();
58
+ }
59
+ }
60
+ /**
61
+ * Stop all background services
62
+ */
63
+ dispose() {
64
+ if (this.refreshIntervalId)
65
+ clearInterval(this.refreshIntervalId);
66
+ if (this.statisticsIntervalId)
67
+ clearInterval(this.statisticsIntervalId);
68
+ this.listeners.get("update").clear();
69
+ this.listeners.get("ready").clear();
70
+ }
71
+ /**
72
+ * Register a hook that will be called for every flag evaluation.
73
+ */
74
+ addHook(hook) {
75
+ this.hooks.push(hook);
76
+ }
77
+ /**
78
+ * Check if a feature is enabled (no context)
79
+ */
80
+ async isEnabled(featureName) {
81
+ return this.isEnabledWithContext(featureName, {});
82
+ }
83
+ /**
84
+ * Check if a feature is enabled with context properties evaluated locally
85
+ */
86
+ async isEnabledWithContext(featureName, context) {
87
+ const hookCtx = {
88
+ flagKey: featureName,
89
+ evaluationContext: Object.keys(context).length > 0 ? context : undefined,
90
+ defaultValue: false,
91
+ };
92
+ await this.invokeHooksBefore(hookCtx);
93
+ const entry = this.manifest.get(featureName.toLowerCase()) ??
94
+ this.findEntry(featureName);
95
+ if (!entry) {
96
+ const defaultResult = {
97
+ value: false,
98
+ reason: "default",
99
+ };
100
+ await this.invokeHooksAfter(hookCtx, defaultResult);
101
+ return false;
102
+ }
103
+ const result = this.evaluateEntry(entry, context);
104
+ const reason = this.determineReason(entry, context, result);
105
+ const evalResult = { value: result, reason };
106
+ await this.invokeHooksAfter(hookCtx, evalResult);
107
+ this.trackFeatureHit(featureName, result);
108
+ return result;
109
+ }
110
+ determineReason(entry, context, _result) {
111
+ if (!entry.enabled)
112
+ return "disabled";
113
+ if (!entry.conditions || entry.conditions.length === 0)
114
+ return "static";
115
+ return Object.keys(context).length > 0 ? "targeting_match" : "static";
116
+ }
117
+ async invokeHooksBefore(context) {
118
+ for (const hook of this.hooks) {
119
+ try {
120
+ await hook.before?.(context);
121
+ }
122
+ catch {
123
+ /* hook errors are swallowed */
124
+ }
125
+ }
126
+ }
127
+ async invokeHooksAfter(context, result) {
128
+ for (const hook of this.hooks) {
129
+ try {
130
+ await hook.after?.(context, result);
131
+ }
132
+ catch {
133
+ /* hook errors are swallowed */
134
+ }
135
+ }
136
+ }
137
+ async invokeHooksError(context, error) {
138
+ for (const hook of this.hooks) {
139
+ try {
140
+ await hook.error?.(context, error);
141
+ }
142
+ catch {
143
+ /* hook errors are swallowed */
144
+ }
145
+ }
146
+ }
147
+ // ─── Local evaluation ───────────────────────────────────────────────────────
148
+ findEntry(featureName) {
149
+ for (const [key, value] of this.manifest.entries()) {
150
+ if (key.toLowerCase() === featureName.toLowerCase())
151
+ return value;
152
+ }
153
+ return undefined;
154
+ }
155
+ evaluateEntry(entry, context) {
156
+ if (!entry.enabled)
157
+ return false;
158
+ if (!entry.conditions || entry.conditions.length === 0)
159
+ return true;
160
+ return this.evaluateConditions(entry.conditions, context);
161
+ }
162
+ evaluateConditions(conditions, context) {
163
+ if (conditions.length === 0)
164
+ return true;
165
+ // Split the flat list into AND-segments at every OR boundary.
166
+ // AND binds tighter than OR — (A AND B OR C) = (A AND B) OR (C).
167
+ // The overall expression is true if ANY segment is fully satisfied.
168
+ const segments = [];
169
+ let current = [];
170
+ for (let i = 0; i < conditions.length; i++) {
171
+ current.push(conditions[i]);
172
+ const logic = (conditions[i].logicAfter ?? "AND").toUpperCase();
173
+ if (logic === "OR" || i === conditions.length - 1) {
174
+ segments.push(current);
175
+ current = [];
176
+ }
177
+ }
178
+ return segments.some((seg) => this.evaluateAndSegment(seg, context));
179
+ }
180
+ /** Returns true only when ALL rules in the segment match. */
181
+ evaluateAndSegment(rules, context) {
182
+ for (const rule of rules) {
183
+ const providedValue = context[rule.contextProperty];
184
+ if (providedValue === undefined)
185
+ return false;
186
+ let match;
187
+ if (rule.conditionGroupValues && rule.conditionGroupValues.length > 0) {
188
+ match = this.evaluateValueListCondition(rule.operator, rule.conditionGroupValues, providedValue);
189
+ }
190
+ else if (rule.value !== undefined && rule.value !== null) {
191
+ match = this.evaluateDirectValueCondition(rule.operator, rule.value, providedValue);
192
+ }
193
+ else {
194
+ return false; // invalid rule
195
+ }
196
+ if (!match)
197
+ return false;
198
+ }
199
+ return true;
200
+ }
201
+ evaluateDirectValueCondition(op, ruleValue, providedValue) {
202
+ switch (op) {
203
+ case "Contains": {
204
+ const ruleValues = ruleValue
205
+ .split(",")
206
+ .map((v) => v.trim().toLowerCase());
207
+ return providedValue
208
+ .split(",")
209
+ .some((v) => ruleValues.includes(v.trim().toLowerCase()));
210
+ }
211
+ case "NotContains": {
212
+ const ruleValues = ruleValue
213
+ .split(",")
214
+ .map((v) => v.trim().toLowerCase());
215
+ return !providedValue
216
+ .split(",")
217
+ .some((v) => ruleValues.includes(v.trim().toLowerCase()));
218
+ }
219
+ case "Match":
220
+ return (this.orderCommaSeparated(ruleValue) ===
221
+ this.orderCommaSeparated(providedValue));
222
+ case "NotMatch":
223
+ return (this.orderCommaSeparated(ruleValue) !==
224
+ this.orderCommaSeparated(providedValue));
225
+ case "GreaterThan": {
226
+ const rv = parseInt(ruleValue, 10);
227
+ const pv = parseInt(providedValue, 10);
228
+ return !isNaN(rv) && !isNaN(pv) && pv > rv;
229
+ }
230
+ case "LessThan": {
231
+ const rv = parseInt(ruleValue, 10);
232
+ const pv = parseInt(providedValue, 10);
233
+ return !isNaN(rv) && !isNaN(pv) && pv < rv;
234
+ }
235
+ default:
236
+ return false;
237
+ }
238
+ }
239
+ evaluateValueListCondition(op, values, providedValue) {
240
+ const normalised = values.map((v) => v.toLowerCase());
241
+ switch (op) {
242
+ case "Contains":
243
+ return providedValue
244
+ .split(",")
245
+ .some((v) => normalised.includes(v.trim().toLowerCase()));
246
+ case "NotContains":
247
+ return !providedValue
248
+ .split(",")
249
+ .some((v) => normalised.includes(v.trim().toLowerCase()));
250
+ case "Match":
251
+ return ([...values]
252
+ .sort((a, b) => a.localeCompare(b))
253
+ .join(",")
254
+ .toLowerCase() === this.orderCommaSeparated(providedValue));
255
+ case "NotMatch":
256
+ return ([...values]
257
+ .sort((a, b) => a.localeCompare(b))
258
+ .join(",")
259
+ .toLowerCase() !== this.orderCommaSeparated(providedValue));
260
+ default:
261
+ return false;
262
+ }
263
+ }
264
+ orderCommaSeparated(value) {
265
+ return value
266
+ .split(",")
267
+ .map((v) => v.trim().toLowerCase())
268
+ .sort()
269
+ .join(",");
270
+ }
271
+ // ─── Manifest management ────────────────────────────────────────────────────
272
+ async refreshManifest() {
273
+ try {
274
+ const manifest = await this.fetchManifestFromApi();
275
+ // Atomic swap — replace entire map reference
276
+ const newMap = new Map();
277
+ for (const entry of manifest.features) {
278
+ newMap.set(entry.name.toLowerCase(), entry);
279
+ }
280
+ this.manifest = newMap;
281
+ if (this.cacheOptions.enableLocalStoragePersistence) {
282
+ this.saveManifestToStorage(manifest);
283
+ }
284
+ this.emit("update");
285
+ }
286
+ catch (error) {
287
+ console.error("Failed to refresh feature manifest:", error);
288
+ }
289
+ }
290
+ async fetchManifestFromApi() {
291
+ const url = `${this.config.baseUrl}/feature/manifest`;
292
+ const response = await fetch(url, {
293
+ method: "GET",
294
+ headers: {
295
+ "Content-Type": "application/json",
296
+ "X-API-Key": this.config.appKey,
297
+ },
298
+ });
299
+ if (!response.ok) {
300
+ throw new Error(`Manifest fetch returned ${response.status}: ${response.statusText}`);
301
+ }
302
+ return response.json();
303
+ }
304
+ // ─── Background services ────────────────────────────────────────────────────
305
+ startBackgroundRefresh() {
306
+ const intervalMs = this.cacheOptions.refreshIntervalSeconds * 1000;
307
+ this.refreshIntervalId = setInterval(async () => {
308
+ await this.refreshManifest();
309
+ }, intervalMs);
310
+ }
311
+ startStatisticsReporting() {
312
+ // Report statistics at the same interval as manifest refresh
313
+ const intervalMs = this.cacheOptions.refreshIntervalSeconds * 1000;
314
+ this.statisticsIntervalId = setInterval(async () => {
315
+ await this.sendStatistics();
316
+ }, intervalMs);
317
+ }
318
+ // ─── Statistics ─────────────────────────────────────────────────────────────
319
+ trackFeatureHit(featureName, wasEnabled) {
320
+ if (!this.config.sendStatistics)
321
+ return;
322
+ this.featureHitCounts.set(featureName, (this.featureHitCounts.get(featureName) || 0) + 1);
323
+ if (wasEnabled) {
324
+ this.featureEnabledCounts.set(featureName, (this.featureEnabledCounts.get(featureName) || 0) + 1);
325
+ }
326
+ else {
327
+ this.featureDisabledCounts.set(featureName, (this.featureDisabledCounts.get(featureName) || 0) + 1);
328
+ }
329
+ this.featureLastAccessed.set(featureName, new Date());
330
+ }
331
+ async sendStatistics() {
332
+ if (!this.config.sendStatistics)
333
+ return;
334
+ if (this.featureHitCounts.size === 0)
335
+ return;
336
+ try {
337
+ const features = [];
338
+ for (const [featureName, hitCount] of this.featureHitCounts.entries()) {
339
+ features.push({
340
+ featureName,
341
+ hitCount,
342
+ enabledCount: this.featureEnabledCounts.get(featureName) || 0,
343
+ disabledCount: this.featureDisabledCounts.get(featureName) || 0,
344
+ lastAccessed: this.featureLastAccessed.get(featureName) || new Date(),
345
+ });
346
+ }
347
+ const report = {
348
+ appKey: this.config.appKey,
349
+ features,
350
+ reportTimestamp: new Date(),
351
+ };
352
+ const response = await fetch(`${this.config.baseUrl}/api/Statistics/Report`, {
353
+ method: "POST",
354
+ headers: {
355
+ "Content-Type": "application/json",
356
+ "X-API-Key": this.config.appKey,
357
+ },
358
+ body: JSON.stringify(report),
359
+ });
360
+ if (response.ok) {
361
+ this.resetStatistics();
362
+ }
363
+ else {
364
+ console.warn(`Failed to send statistics: ${response.status}`);
365
+ }
366
+ }
367
+ catch (error) {
368
+ console.error("Error sending statistics:", error);
369
+ }
370
+ }
371
+ resetStatistics() {
372
+ this.featureHitCounts.clear();
373
+ this.featureEnabledCounts.clear();
374
+ this.featureDisabledCounts.clear();
375
+ this.featureLastAccessed.clear();
376
+ }
377
+ getStatistics() {
378
+ const stats = [];
379
+ for (const [featureName, hitCount] of this.featureHitCounts.entries()) {
380
+ stats.push({
381
+ featureName,
382
+ hitCount,
383
+ enabledCount: this.featureEnabledCounts.get(featureName) || 0,
384
+ disabledCount: this.featureDisabledCounts.get(featureName) || 0,
385
+ lastAccessed: this.featureLastAccessed.get(featureName) || new Date(),
386
+ });
387
+ }
388
+ return stats;
389
+ }
390
+ // ─── LocalStorage persistence ───────────────────────────────────────────────
391
+ saveManifestToStorage(manifest) {
392
+ if (typeof window === "undefined" || !window.localStorage)
393
+ return;
394
+ try {
395
+ localStorage.setItem(this.cacheOptions.localStorageKey, JSON.stringify(manifest));
396
+ }
397
+ catch (error) {
398
+ console.error("Failed to persist manifest to localStorage:", error);
399
+ }
400
+ }
401
+ async loadManifestFromStorage() {
402
+ if (typeof window === "undefined" || !window.localStorage)
403
+ return;
404
+ try {
405
+ const stored = localStorage.getItem(this.cacheOptions.localStorageKey);
406
+ if (!stored)
407
+ return;
408
+ const manifest = JSON.parse(stored);
409
+ if (!manifest?.features?.length)
410
+ return;
411
+ const newMap = new Map();
412
+ for (const entry of manifest.features) {
413
+ newMap.set(entry.name.toLowerCase(), entry);
414
+ }
415
+ this.manifest = newMap;
416
+ }
417
+ catch (error) {
418
+ console.error("Failed to load manifest from localStorage:", error);
419
+ }
420
+ }
421
+ clearCache() {
422
+ this.manifest.clear();
423
+ if (typeof window !== "undefined" && window.localStorage) {
424
+ localStorage.removeItem(this.cacheOptions.localStorageKey);
425
+ }
426
+ }
427
+ }
@@ -0,0 +1,32 @@
1
+ import { OnInit, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
2
+ import { FtureXService } from './fturex.service.js';
3
+ import { ContextProperties } from '../types.js';
4
+ /**
5
+ * Structural directive that conditionally renders a template based on a feature toggle.
6
+ *
7
+ * @example
8
+ * <!-- Simple usage -->
9
+ * <div *ftureX="'new-dashboard'">New dashboard content</div>
10
+ *
11
+ * <!-- With else template -->
12
+ * <div *ftureX="'new-dashboard'; else legacyTpl">New content</div>
13
+ * <ng-template #legacyTpl>Legacy content</ng-template>
14
+ *
15
+ * <!-- With context and fallback -->
16
+ * <div *ftureX="'beta-feature'; context: { role: 'admin' }; fallback: false">
17
+ * Beta content
18
+ * </div>
19
+ */
20
+ export declare class FeatureToggleDirective implements OnInit, OnDestroy {
21
+ private readonly templateRef;
22
+ private readonly viewContainer;
23
+ private readonly ftureXService;
24
+ featureName: string;
25
+ context?: ContextProperties;
26
+ elseTemplate?: TemplateRef<unknown>;
27
+ private subscription?;
28
+ private hasView;
29
+ constructor(templateRef: TemplateRef<unknown>, viewContainer: ViewContainerRef, ftureXService: FtureXService);
30
+ ngOnInit(): void;
31
+ ngOnDestroy(): void;
32
+ }
@@ -0,0 +1,77 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
11
+ import { FtureXService } from './fturex.service.js';
12
+ /**
13
+ * Structural directive that conditionally renders a template based on a feature toggle.
14
+ *
15
+ * @example
16
+ * <!-- Simple usage -->
17
+ * <div *ftureX="'new-dashboard'">New dashboard content</div>
18
+ *
19
+ * <!-- With else template -->
20
+ * <div *ftureX="'new-dashboard'; else legacyTpl">New content</div>
21
+ * <ng-template #legacyTpl>Legacy content</ng-template>
22
+ *
23
+ * <!-- With context and fallback -->
24
+ * <div *ftureX="'beta-feature'; context: { role: 'admin' }; fallback: false">
25
+ * Beta content
26
+ * </div>
27
+ */
28
+ let FeatureToggleDirective = class FeatureToggleDirective {
29
+ constructor(templateRef, viewContainer, ftureXService) {
30
+ this.templateRef = templateRef;
31
+ this.viewContainer = viewContainer;
32
+ this.ftureXService = ftureXService;
33
+ this.featureName = '';
34
+ this.hasView = false;
35
+ }
36
+ ngOnInit() {
37
+ const obs$ = this.context
38
+ ? this.ftureXService.isEnabledWithContext(this.featureName, this.context)
39
+ : this.ftureXService.isEnabled(this.featureName);
40
+ this.subscription = obs$.subscribe((isEnabled) => {
41
+ if (isEnabled && !this.hasView) {
42
+ this.viewContainer.clear();
43
+ this.viewContainer.createEmbeddedView(this.templateRef);
44
+ this.hasView = true;
45
+ }
46
+ else if (!isEnabled) {
47
+ this.viewContainer.clear();
48
+ this.hasView = false;
49
+ if (this.elseTemplate) {
50
+ this.viewContainer.createEmbeddedView(this.elseTemplate);
51
+ }
52
+ }
53
+ });
54
+ }
55
+ ngOnDestroy() {
56
+ this.subscription?.unsubscribe();
57
+ }
58
+ };
59
+ __decorate([
60
+ Input('ftureX'),
61
+ __metadata("design:type", Object)
62
+ ], FeatureToggleDirective.prototype, "featureName", void 0);
63
+ __decorate([
64
+ Input('ftureXContext'),
65
+ __metadata("design:type", Object)
66
+ ], FeatureToggleDirective.prototype, "context", void 0);
67
+ __decorate([
68
+ Input('ftureXElse'),
69
+ __metadata("design:type", TemplateRef)
70
+ ], FeatureToggleDirective.prototype, "elseTemplate", void 0);
71
+ FeatureToggleDirective = __decorate([
72
+ Directive({ selector: '[ftureX]' }),
73
+ __metadata("design:paramtypes", [TemplateRef,
74
+ ViewContainerRef,
75
+ FtureXService])
76
+ ], FeatureToggleDirective);
77
+ export { FeatureToggleDirective };
@@ -0,0 +1,20 @@
1
+ import { PipeTransform } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import { FtureXService } from './fturex.service.js';
4
+ import { ContextProperties } from '../types.js';
5
+ /**
6
+ * Async pipe that resolves to a boolean indicating whether a feature is enabled.
7
+ * Must be used with the async pipe in templates.
8
+ *
9
+ * @example
10
+ * <!-- In template -->
11
+ * <div *ngIf="'new-dashboard' | ftureX | async">New dashboard</div>
12
+ *
13
+ * <!-- With context -->
14
+ * <div *ngIf="'beta-feature' | ftureX: { role: 'admin' } | async">Beta</div>
15
+ */
16
+ export declare class FeatureTogglePipe implements PipeTransform {
17
+ private readonly ftureXService;
18
+ constructor(ftureXService: FtureXService);
19
+ transform(featureName: string, context?: ContextProperties): Observable<boolean>;
20
+ }
@@ -0,0 +1,37 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Pipe } from '@angular/core';
11
+ import { FtureXService } from './fturex.service.js';
12
+ /**
13
+ * Async pipe that resolves to a boolean indicating whether a feature is enabled.
14
+ * Must be used with the async pipe in templates.
15
+ *
16
+ * @example
17
+ * <!-- In template -->
18
+ * <div *ngIf="'new-dashboard' | ftureX | async">New dashboard</div>
19
+ *
20
+ * <!-- With context -->
21
+ * <div *ngIf="'beta-feature' | ftureX: { role: 'admin' } | async">Beta</div>
22
+ */
23
+ let FeatureTogglePipe = class FeatureTogglePipe {
24
+ constructor(ftureXService) {
25
+ this.ftureXService = ftureXService;
26
+ }
27
+ transform(featureName, context) {
28
+ return context
29
+ ? this.ftureXService.isEnabledWithContext(featureName, context)
30
+ : this.ftureXService.isEnabled(featureName);
31
+ }
32
+ };
33
+ FeatureTogglePipe = __decorate([
34
+ Pipe({ name: 'ftureX', pure: true }),
35
+ __metadata("design:paramtypes", [FtureXService])
36
+ ], FeatureTogglePipe);
37
+ export { FeatureTogglePipe };
@@ -0,0 +1,7 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { FtureXConfiguration, FeatureCacheOptions } from '../types.js';
3
+ export interface FtureXModuleConfig {
4
+ config: FtureXConfiguration;
5
+ cacheOptions?: FeatureCacheOptions;
6
+ }
7
+ export declare const FTUREX_CONFIG: InjectionToken<FtureXModuleConfig>;
@@ -0,0 +1,2 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ export const FTUREX_CONFIG = new InjectionToken('FTUREX_CONFIG');
@@ -0,0 +1,23 @@
1
+ import { ModuleWithProviders } from '@angular/core';
2
+ import { FtureXModuleConfig } from './fturex.config.js';
3
+ /**
4
+ * Angular module for FtureX feature toggles.
5
+ *
6
+ * @example
7
+ * // app.module.ts
8
+ * @NgModule({
9
+ * imports: [
10
+ * FtureXModule.forRoot({
11
+ * config: {
12
+ * baseUrl: 'https://api.example.com',
13
+ * appKey: 'your-api-key',
14
+ * },
15
+ * cacheOptions: { refreshIntervalSeconds: 60 },
16
+ * }),
17
+ * ],
18
+ * })
19
+ * export class AppModule {}
20
+ */
21
+ export declare class FtureXModule {
22
+ static forRoot(moduleConfig: FtureXModuleConfig): ModuleWithProviders<FtureXModule>;
23
+ }
@@ -0,0 +1,48 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var FtureXModule_1;
8
+ import { NgModule } from '@angular/core';
9
+ import { FtureXService } from './fturex.service.js';
10
+ import { FeatureToggleDirective } from './feature-toggle.directive.js';
11
+ import { FeatureTogglePipe } from './feature-toggle.pipe.js';
12
+ import { FTUREX_CONFIG } from './fturex.config.js';
13
+ /**
14
+ * Angular module for FtureX feature toggles.
15
+ *
16
+ * @example
17
+ * // app.module.ts
18
+ * @NgModule({
19
+ * imports: [
20
+ * FtureXModule.forRoot({
21
+ * config: {
22
+ * baseUrl: 'https://api.example.com',
23
+ * appKey: 'your-api-key',
24
+ * },
25
+ * cacheOptions: { refreshIntervalSeconds: 60 },
26
+ * }),
27
+ * ],
28
+ * })
29
+ * export class AppModule {}
30
+ */
31
+ let FtureXModule = FtureXModule_1 = class FtureXModule {
32
+ static forRoot(moduleConfig) {
33
+ return {
34
+ ngModule: FtureXModule_1,
35
+ providers: [
36
+ { provide: FTUREX_CONFIG, useValue: moduleConfig },
37
+ FtureXService,
38
+ ],
39
+ };
40
+ }
41
+ };
42
+ FtureXModule = FtureXModule_1 = __decorate([
43
+ NgModule({
44
+ declarations: [FeatureToggleDirective, FeatureTogglePipe],
45
+ exports: [FeatureToggleDirective, FeatureTogglePipe],
46
+ })
47
+ ], FtureXModule);
48
+ export { FtureXModule };