@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.
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/dist/FtureXClient.d.ts +72 -0
- package/dist/FtureXClient.js +427 -0
- package/dist/angular/feature-toggle.directive.d.ts +32 -0
- package/dist/angular/feature-toggle.directive.js +77 -0
- package/dist/angular/feature-toggle.pipe.d.ts +20 -0
- package/dist/angular/feature-toggle.pipe.js +37 -0
- package/dist/angular/fturex.config.d.ts +7 -0
- package/dist/angular/fturex.config.js +2 -0
- package/dist/angular/fturex.module.d.ts +23 -0
- package/dist/angular/fturex.module.js +48 -0
- package/dist/angular/fturex.service.d.ts +31 -0
- package/dist/angular/fturex.service.js +56 -0
- package/dist/angular/index.d.ts +6 -0
- package/dist/angular/index.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/opentelemetry/FtureXOtelHook.d.ts +58 -0
- package/dist/opentelemetry/FtureXOtelHook.js +86 -0
- package/dist/opentelemetry/index.d.ts +2 -0
- package/dist/opentelemetry/index.js +1 -0
- package/dist/react/FeatureToggle.d.ts +14 -0
- package/dist/react/FeatureToggle.js +16 -0
- package/dist/react/FeatureToggleProvider.d.ts +15 -0
- package/dist/react/FeatureToggleProvider.js +17 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useFeatureToggle.d.ts +27 -0
- package/dist/react/useFeatureToggle.js +104 -0
- package/dist/svelte/index.d.ts +2 -0
- package/dist/svelte/index.js +1 -0
- package/dist/svelte/useFeatureToggle.d.ts +54 -0
- package/dist/svelte/useFeatureToggle.js +85 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +1 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +2 -0
- package/dist/vue/useFeatureToggle.d.ts +32 -0
- package/dist/vue/useFeatureToggle.js +104 -0
- package/package.json +99 -0
- package/src/vue/FeatureToggle.vue +28 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { OnDestroy } from '@angular/core';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { ContextProperties } from '../types.js';
|
|
4
|
+
import { FtureXModuleConfig } from './fturex.config.js';
|
|
5
|
+
/**
|
|
6
|
+
* Angular injectable service wrapping FtureXClient.
|
|
7
|
+
* Provide via FtureXModule.forRoot() in your AppModule.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* constructor(private ftureX: FtureXService) {}
|
|
11
|
+
*
|
|
12
|
+
* ngOnInit() {
|
|
13
|
+
* this.ftureX.isEnabled('new-dashboard').subscribe(enabled => {
|
|
14
|
+
* this.showNewDashboard = enabled;
|
|
15
|
+
* });
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
export declare class FtureXService implements OnDestroy {
|
|
19
|
+
private readonly client;
|
|
20
|
+
readonly initialized: Promise<void>;
|
|
21
|
+
constructor(moduleConfig: FtureXModuleConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Check if a feature is enabled (no context)
|
|
24
|
+
*/
|
|
25
|
+
isEnabled(featureName: string): Observable<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Check if a feature is enabled with context properties
|
|
28
|
+
*/
|
|
29
|
+
isEnabledWithContext(featureName: string, context: ContextProperties): Observable<boolean>;
|
|
30
|
+
ngOnDestroy(): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Injectable, Inject } from '@angular/core';
|
|
14
|
+
import { from } from 'rxjs';
|
|
15
|
+
import { FtureXClient } from '../FtureXClient.js';
|
|
16
|
+
import { FTUREX_CONFIG } from './fturex.config.js';
|
|
17
|
+
/**
|
|
18
|
+
* Angular injectable service wrapping FtureXClient.
|
|
19
|
+
* Provide via FtureXModule.forRoot() in your AppModule.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* constructor(private ftureX: FtureXService) {}
|
|
23
|
+
*
|
|
24
|
+
* ngOnInit() {
|
|
25
|
+
* this.ftureX.isEnabled('new-dashboard').subscribe(enabled => {
|
|
26
|
+
* this.showNewDashboard = enabled;
|
|
27
|
+
* });
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
let FtureXService = class FtureXService {
|
|
31
|
+
constructor(moduleConfig) {
|
|
32
|
+
this.client = new FtureXClient(moduleConfig.config, moduleConfig.cacheOptions);
|
|
33
|
+
this.initialized = this.client.initialize();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if a feature is enabled (no context)
|
|
37
|
+
*/
|
|
38
|
+
isEnabled(featureName) {
|
|
39
|
+
return from(this.client.isEnabled(featureName));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if a feature is enabled with context properties
|
|
43
|
+
*/
|
|
44
|
+
isEnabledWithContext(featureName, context) {
|
|
45
|
+
return from(this.client.isEnabledWithContext(featureName, context));
|
|
46
|
+
}
|
|
47
|
+
ngOnDestroy() {
|
|
48
|
+
this.client.dispose();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
FtureXService = __decorate([
|
|
52
|
+
Injectable(),
|
|
53
|
+
__param(0, Inject(FTUREX_CONFIG)),
|
|
54
|
+
__metadata("design:paramtypes", [Object])
|
|
55
|
+
], FtureXService);
|
|
56
|
+
export { FtureXService };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { FtureXModule } from './fturex.module.js';
|
|
2
|
+
export { FtureXService } from './fturex.service.js';
|
|
3
|
+
export { FeatureToggleDirective } from './feature-toggle.directive.js';
|
|
4
|
+
export { FeatureTogglePipe } from './feature-toggle.pipe.js';
|
|
5
|
+
export { FTUREX_CONFIG } from './fturex.config.js';
|
|
6
|
+
export type { FtureXModuleConfig } from './fturex.config.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { FtureXModule } from './fturex.module.js';
|
|
2
|
+
export { FtureXService } from './fturex.service.js';
|
|
3
|
+
export { FeatureToggleDirective } from './feature-toggle.directive.js';
|
|
4
|
+
export { FeatureTogglePipe } from './feature-toggle.pipe.js';
|
|
5
|
+
export { FTUREX_CONFIG } from './fturex.config.js';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { FtureXHook, HookContext, EvaluationResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for the FtureX OpenTelemetry hook.
|
|
4
|
+
*/
|
|
5
|
+
export interface FtureXOtelOptions {
|
|
6
|
+
/** Add span events to the current active span on each evaluation. Default: true */
|
|
7
|
+
addSpanEvents?: boolean;
|
|
8
|
+
/** Emit evaluation counters as OTel metrics. Default: true */
|
|
9
|
+
addMetrics?: boolean;
|
|
10
|
+
/** Include the evaluated flag value in span event attributes. Default: false */
|
|
11
|
+
recordFlagValue?: boolean;
|
|
12
|
+
/** Optional identifier for the feature flag set (e.g. environment name). */
|
|
13
|
+
setId?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Minimal OTel API surface used by this hook.
|
|
17
|
+
* Compatible with `@opentelemetry/api` — pass the module directly.
|
|
18
|
+
*/
|
|
19
|
+
export interface OtelApi {
|
|
20
|
+
trace: {
|
|
21
|
+
getActiveSpan(): {
|
|
22
|
+
addEvent(name: string, attributes?: Record<string, string>): void;
|
|
23
|
+
} | undefined;
|
|
24
|
+
};
|
|
25
|
+
metrics: {
|
|
26
|
+
getMeter(name: string): {
|
|
27
|
+
createCounter(name: string, options?: {
|
|
28
|
+
description?: string;
|
|
29
|
+
}): {
|
|
30
|
+
add(value: number, attributes?: Record<string, string>): void;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* OpenTelemetry hook for FtureX.
|
|
37
|
+
* Emits span events and metrics following OTel semantic conventions for feature flags.
|
|
38
|
+
*
|
|
39
|
+
* Requires `@opentelemetry/api` as a peer dependency.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { FtureXOtelHook } from 'fturex/opentelemetry';
|
|
44
|
+
* import * as api from '@opentelemetry/api';
|
|
45
|
+
*
|
|
46
|
+
* const otelHook = new FtureXOtelHook(api, { recordFlagValue: true });
|
|
47
|
+
* client.addHook(otelHook);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare class FtureXOtelHook implements FtureXHook {
|
|
51
|
+
private readonly options;
|
|
52
|
+
private readonly otel;
|
|
53
|
+
private evalSuccessCounter?;
|
|
54
|
+
private evalErrorCounter?;
|
|
55
|
+
constructor(otel: OtelApi, options?: FtureXOtelOptions);
|
|
56
|
+
after(context: HookContext, result: EvaluationResult): void;
|
|
57
|
+
error(context: HookContext, error: unknown): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry hook for FtureX.
|
|
3
|
+
* Emits span events and metrics following OTel semantic conventions for feature flags.
|
|
4
|
+
*
|
|
5
|
+
* Requires `@opentelemetry/api` as a peer dependency.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { FtureXOtelHook } from 'fturex/opentelemetry';
|
|
10
|
+
* import * as api from '@opentelemetry/api';
|
|
11
|
+
*
|
|
12
|
+
* const otelHook = new FtureXOtelHook(api, { recordFlagValue: true });
|
|
13
|
+
* client.addHook(otelHook);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class FtureXOtelHook {
|
|
17
|
+
constructor(otel, options = {}) {
|
|
18
|
+
this.otel = otel;
|
|
19
|
+
this.options = {
|
|
20
|
+
addSpanEvents: options.addSpanEvents ?? true,
|
|
21
|
+
addMetrics: options.addMetrics ?? true,
|
|
22
|
+
recordFlagValue: options.recordFlagValue ?? false,
|
|
23
|
+
setId: options.setId ?? '',
|
|
24
|
+
};
|
|
25
|
+
if (this.options.addMetrics) {
|
|
26
|
+
const meter = otel.metrics.getMeter('fturex');
|
|
27
|
+
this.evalSuccessCounter = meter.createCounter('feature_flag.evaluation_success_total', {
|
|
28
|
+
description: 'Number of successful feature flag evaluations',
|
|
29
|
+
});
|
|
30
|
+
this.evalErrorCounter = meter.createCounter('feature_flag.evaluation_error_total', {
|
|
31
|
+
description: 'Number of failed feature flag evaluations',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
after(context, result) {
|
|
36
|
+
if (this.options.addSpanEvents) {
|
|
37
|
+
const span = this.otel.trace.getActiveSpan();
|
|
38
|
+
if (span) {
|
|
39
|
+
const attributes = {
|
|
40
|
+
'feature_flag.key': context.flagKey,
|
|
41
|
+
'feature_flag.provider.name': 'FtureX',
|
|
42
|
+
'feature_flag.result.reason': result.reason,
|
|
43
|
+
};
|
|
44
|
+
if (this.options.recordFlagValue) {
|
|
45
|
+
attributes['feature_flag.result.value'] = String(result.value);
|
|
46
|
+
}
|
|
47
|
+
if (this.options.setId) {
|
|
48
|
+
attributes['feature_flag.set.id'] = this.options.setId;
|
|
49
|
+
}
|
|
50
|
+
if (context.evaluationContext) {
|
|
51
|
+
const contextId = context.evaluationContext['userId'] ?? context.evaluationContext['tenantId'];
|
|
52
|
+
if (contextId) {
|
|
53
|
+
attributes['feature_flag.context.id'] = contextId;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
span.addEvent('feature_flag.evaluation', attributes);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (this.options.addMetrics && this.evalSuccessCounter) {
|
|
60
|
+
this.evalSuccessCounter.add(1, {
|
|
61
|
+
'feature_flag.key': context.flagKey,
|
|
62
|
+
'feature_flag.provider.name': 'FtureX',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
error(context, error) {
|
|
67
|
+
if (this.options.addSpanEvents) {
|
|
68
|
+
const span = this.otel.trace.getActiveSpan();
|
|
69
|
+
if (span) {
|
|
70
|
+
span.addEvent('feature_flag.evaluation', {
|
|
71
|
+
'feature_flag.key': context.flagKey,
|
|
72
|
+
'feature_flag.provider.name': 'FtureX',
|
|
73
|
+
'feature_flag.result.reason': 'error',
|
|
74
|
+
'error.type': error instanceof Error ? error.constructor.name : 'Error',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (this.options.addMetrics && this.evalErrorCounter) {
|
|
79
|
+
this.evalErrorCounter.add(1, {
|
|
80
|
+
'feature_flag.key': context.flagKey,
|
|
81
|
+
'feature_flag.provider.name': 'FtureX',
|
|
82
|
+
'error.type': error instanceof Error ? error.constructor.name : 'Error',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FtureXOtelHook } from './FtureXOtelHook.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { ContextProperties } from "../types.js";
|
|
3
|
+
interface FeatureToggleProps {
|
|
4
|
+
featureName: string;
|
|
5
|
+
context?: ContextProperties;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
loading?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* React component for conditional rendering based on feature toggles
|
|
12
|
+
*/
|
|
13
|
+
export declare const FeatureToggle: React.FC<FeatureToggleProps>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useFeatureToggle } from "./useFeatureToggle.js";
|
|
3
|
+
/**
|
|
4
|
+
* React component for conditional rendering based on feature toggles
|
|
5
|
+
*/
|
|
6
|
+
export const FeatureToggle = ({ featureName, context, children, fallback = null, loading: loadingComponent = null, }) => {
|
|
7
|
+
const { isEnabled, loading, error } = useFeatureToggle(featureName, context);
|
|
8
|
+
if (loading) {
|
|
9
|
+
return React.createElement(React.Fragment, null, loadingComponent);
|
|
10
|
+
}
|
|
11
|
+
if (error) {
|
|
12
|
+
console.error(`Error checking feature '${featureName}':`, error);
|
|
13
|
+
return React.createElement(React.Fragment, null, fallback);
|
|
14
|
+
}
|
|
15
|
+
return React.createElement(React.Fragment, null, isEnabled ? children : fallback);
|
|
16
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { FtureXClient } from '../FtureXClient.js';
|
|
3
|
+
import { FtureXConfiguration, FeatureCacheOptions } from '../types.js';
|
|
4
|
+
export declare const FeatureToggleContext: React.Context<FtureXClient | null>;
|
|
5
|
+
interface FeatureToggleProviderProps {
|
|
6
|
+
config: FtureXConfiguration;
|
|
7
|
+
cacheOptions?: FeatureCacheOptions;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* React Provider for Feature Toggle Client
|
|
12
|
+
* Wrap your app with this provider to enable feature toggle hooks
|
|
13
|
+
*/
|
|
14
|
+
export declare const FeatureToggleProvider: React.FC<FeatureToggleProviderProps>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { createContext, useEffect, useState } from 'react';
|
|
2
|
+
import { FtureXClient } from '../FtureXClient.js';
|
|
3
|
+
export const FeatureToggleContext = createContext(null);
|
|
4
|
+
/**
|
|
5
|
+
* React Provider for Feature Toggle Client
|
|
6
|
+
* Wrap your app with this provider to enable feature toggle hooks
|
|
7
|
+
*/
|
|
8
|
+
export const FeatureToggleProvider = ({ config, cacheOptions, children }) => {
|
|
9
|
+
const [client] = useState(() => new FtureXClient(config, cacheOptions));
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
client.initialize();
|
|
12
|
+
return () => {
|
|
13
|
+
client.dispose();
|
|
14
|
+
};
|
|
15
|
+
}, [client]);
|
|
16
|
+
return (React.createElement(FeatureToggleContext.Provider, { value: client }, children));
|
|
17
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ContextProperties } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* React hook for checking feature toggles.
|
|
4
|
+
* Re-evaluates automatically whenever the client refreshes its manifest,
|
|
5
|
+
* so the UI updates without a page reload.
|
|
6
|
+
*
|
|
7
|
+
* @param featureName - Name of the feature to check
|
|
8
|
+
* @param context - Optional context properties for the feature check
|
|
9
|
+
* @returns Object with isEnabled state, loading state, and error
|
|
10
|
+
*/
|
|
11
|
+
export declare function useFeatureToggle(featureName: string, context?: ContextProperties): {
|
|
12
|
+
isEnabled: boolean;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* React hook for checking multiple feature toggles at once.
|
|
18
|
+
* Re-evaluates automatically whenever the client refreshes its manifest.
|
|
19
|
+
*
|
|
20
|
+
* @param featureNames - Array of feature names to check
|
|
21
|
+
* @returns Object with features map, loading state, and error
|
|
22
|
+
*/
|
|
23
|
+
export declare function useFeatureToggles(featureNames: string[]): {
|
|
24
|
+
features: Record<string, boolean>;
|
|
25
|
+
loading: boolean;
|
|
26
|
+
error: Error | null;
|
|
27
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useState, useEffect, useContext } from 'react';
|
|
2
|
+
import { FeatureToggleContext } from './FeatureToggleProvider.js';
|
|
3
|
+
/**
|
|
4
|
+
* React hook for checking feature toggles.
|
|
5
|
+
* Re-evaluates automatically whenever the client refreshes its manifest,
|
|
6
|
+
* so the UI updates without a page reload.
|
|
7
|
+
*
|
|
8
|
+
* @param featureName - Name of the feature to check
|
|
9
|
+
* @param context - Optional context properties for the feature check
|
|
10
|
+
* @returns Object with isEnabled state, loading state, and error
|
|
11
|
+
*/
|
|
12
|
+
export function useFeatureToggle(featureName, context) {
|
|
13
|
+
const client = useContext(FeatureToggleContext);
|
|
14
|
+
const [isEnabled, setIsEnabled] = useState(false);
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!client) {
|
|
19
|
+
setError(new Error('FeatureToggleClient not provided. Wrap your app with FeatureToggleProvider.'));
|
|
20
|
+
setLoading(false);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let mounted = true;
|
|
24
|
+
const evaluate = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const enabled = context
|
|
27
|
+
? await client.isEnabledWithContext(featureName, context)
|
|
28
|
+
: await client.isEnabled(featureName);
|
|
29
|
+
if (mounted) {
|
|
30
|
+
setIsEnabled(enabled);
|
|
31
|
+
setError(null);
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (mounted) {
|
|
37
|
+
setError(err);
|
|
38
|
+
setIsEnabled(false);
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
// Subscribe to manifest updates — re-evaluate whenever the client fetches new data
|
|
44
|
+
client.on('update', evaluate);
|
|
45
|
+
client.on('ready', evaluate);
|
|
46
|
+
// Also evaluate immediately in case the manifest is already loaded
|
|
47
|
+
evaluate();
|
|
48
|
+
return () => {
|
|
49
|
+
mounted = false;
|
|
50
|
+
client.off('update', evaluate);
|
|
51
|
+
client.off('ready', evaluate);
|
|
52
|
+
};
|
|
53
|
+
}, [client, featureName, JSON.stringify(context)]);
|
|
54
|
+
return { isEnabled, loading, error };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* React hook for checking multiple feature toggles at once.
|
|
58
|
+
* Re-evaluates automatically whenever the client refreshes its manifest.
|
|
59
|
+
*
|
|
60
|
+
* @param featureNames - Array of feature names to check
|
|
61
|
+
* @returns Object with features map, loading state, and error
|
|
62
|
+
*/
|
|
63
|
+
export function useFeatureToggles(featureNames) {
|
|
64
|
+
const client = useContext(FeatureToggleContext);
|
|
65
|
+
const [features, setFeatures] = useState({});
|
|
66
|
+
const [loading, setLoading] = useState(true);
|
|
67
|
+
const [error, setError] = useState(null);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!client) {
|
|
70
|
+
setError(new Error('FeatureToggleClient not provided. Wrap your app with FeatureToggleProvider.'));
|
|
71
|
+
setLoading(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let mounted = true;
|
|
75
|
+
const evaluate = async () => {
|
|
76
|
+
try {
|
|
77
|
+
const results = {};
|
|
78
|
+
await Promise.all(featureNames.map(async (name) => {
|
|
79
|
+
results[name] = await client.isEnabled(name);
|
|
80
|
+
}));
|
|
81
|
+
if (mounted) {
|
|
82
|
+
setFeatures(results);
|
|
83
|
+
setError(null);
|
|
84
|
+
setLoading(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (mounted) {
|
|
89
|
+
setError(err);
|
|
90
|
+
setLoading(false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
client.on('update', evaluate);
|
|
95
|
+
client.on('ready', evaluate);
|
|
96
|
+
evaluate();
|
|
97
|
+
return () => {
|
|
98
|
+
mounted = false;
|
|
99
|
+
client.off('update', evaluate);
|
|
100
|
+
client.off('ready', evaluate);
|
|
101
|
+
};
|
|
102
|
+
}, [client, JSON.stringify(featureNames)]);
|
|
103
|
+
return { features, loading, error };
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useFeatureToggle, useFeatureToggles } from './useFeatureToggle.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
import { FtureXClient } from '../FtureXClient.js';
|
|
3
|
+
import { ContextProperties } from '../types.js';
|
|
4
|
+
export interface FeatureToggleState {
|
|
5
|
+
isEnabled: boolean;
|
|
6
|
+
loading: boolean;
|
|
7
|
+
error: Error | null;
|
|
8
|
+
}
|
|
9
|
+
export interface FeatureTogglesState {
|
|
10
|
+
features: Record<string, boolean>;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Creates a Svelte readable store that resolves to a feature toggle state.
|
|
16
|
+
* Re-evaluates automatically whenever the client refreshes its manifest,
|
|
17
|
+
* so the UI updates without a page reload.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* <script>
|
|
21
|
+
* import { useFeatureToggle } from 'fturex/svelte';
|
|
22
|
+
* import { client } from './fturex';
|
|
23
|
+
*
|
|
24
|
+
* const feature = useFeatureToggle(client, 'new-dashboard');
|
|
25
|
+
* </script>
|
|
26
|
+
*
|
|
27
|
+
* {#if $feature.loading}
|
|
28
|
+
* <Spinner />
|
|
29
|
+
* {:else if $feature.isEnabled}
|
|
30
|
+
* <NewDashboard />
|
|
31
|
+
* {:else}
|
|
32
|
+
* <LegacyDashboard />
|
|
33
|
+
* {/if}
|
|
34
|
+
*/
|
|
35
|
+
export declare function useFeatureToggle(client: FtureXClient, featureName: string, context?: ContextProperties): Readable<FeatureToggleState>;
|
|
36
|
+
/**
|
|
37
|
+
* Creates a Svelte readable store that resolves to a map of feature toggle states.
|
|
38
|
+
* Re-evaluates automatically whenever the client refreshes its manifest.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <script>
|
|
42
|
+
* import { useFeatureToggles } from 'fturex/svelte';
|
|
43
|
+
* import { client } from './fturex';
|
|
44
|
+
*
|
|
45
|
+
* const features = useFeatureToggles(client, ['feature-a', 'feature-b']);
|
|
46
|
+
* </script>
|
|
47
|
+
*
|
|
48
|
+
* {#if !$features.loading}
|
|
49
|
+
* {#if $features.features['feature-a']}
|
|
50
|
+
* <FeatureA />
|
|
51
|
+
* {/if}
|
|
52
|
+
* {/if}
|
|
53
|
+
*/
|
|
54
|
+
export declare function useFeatureToggles(client: FtureXClient, featureNames: string[]): Readable<FeatureTogglesState>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Svelte readable store that resolves to a feature toggle state.
|
|
4
|
+
* Re-evaluates automatically whenever the client refreshes its manifest,
|
|
5
|
+
* so the UI updates without a page reload.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <script>
|
|
9
|
+
* import { useFeatureToggle } from 'fturex/svelte';
|
|
10
|
+
* import { client } from './fturex';
|
|
11
|
+
*
|
|
12
|
+
* const feature = useFeatureToggle(client, 'new-dashboard');
|
|
13
|
+
* </script>
|
|
14
|
+
*
|
|
15
|
+
* {#if $feature.loading}
|
|
16
|
+
* <Spinner />
|
|
17
|
+
* {:else if $feature.isEnabled}
|
|
18
|
+
* <NewDashboard />
|
|
19
|
+
* {:else}
|
|
20
|
+
* <LegacyDashboard />
|
|
21
|
+
* {/if}
|
|
22
|
+
*/
|
|
23
|
+
export function useFeatureToggle(client, featureName, context) {
|
|
24
|
+
return readable({ isEnabled: false, loading: true, error: null }, (set) => {
|
|
25
|
+
const evaluate = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const enabled = context
|
|
28
|
+
? await client.isEnabledWithContext(featureName, context)
|
|
29
|
+
: await client.isEnabled(featureName);
|
|
30
|
+
set({ isEnabled: enabled, loading: false, error: null });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
set({ isEnabled: false, loading: false, error: err });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
client.on('update', evaluate);
|
|
37
|
+
client.on('ready', evaluate);
|
|
38
|
+
evaluate();
|
|
39
|
+
return () => {
|
|
40
|
+
client.off('update', evaluate);
|
|
41
|
+
client.off('ready', evaluate);
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a Svelte readable store that resolves to a map of feature toggle states.
|
|
47
|
+
* Re-evaluates automatically whenever the client refreshes its manifest.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* <script>
|
|
51
|
+
* import { useFeatureToggles } from 'fturex/svelte';
|
|
52
|
+
* import { client } from './fturex';
|
|
53
|
+
*
|
|
54
|
+
* const features = useFeatureToggles(client, ['feature-a', 'feature-b']);
|
|
55
|
+
* </script>
|
|
56
|
+
*
|
|
57
|
+
* {#if !$features.loading}
|
|
58
|
+
* {#if $features.features['feature-a']}
|
|
59
|
+
* <FeatureA />
|
|
60
|
+
* {/if}
|
|
61
|
+
* {/if}
|
|
62
|
+
*/
|
|
63
|
+
export function useFeatureToggles(client, featureNames) {
|
|
64
|
+
return readable({ features: {}, loading: true, error: null }, (set) => {
|
|
65
|
+
const evaluate = async () => {
|
|
66
|
+
try {
|
|
67
|
+
const results = {};
|
|
68
|
+
await Promise.all(featureNames.map(async (name) => {
|
|
69
|
+
results[name] = await client.isEnabled(name);
|
|
70
|
+
}));
|
|
71
|
+
set({ features: results, loading: false, error: null });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
set({ features: {}, loading: false, error: err });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
client.on('update', evaluate);
|
|
78
|
+
client.on('ready', evaluate);
|
|
79
|
+
evaluate();
|
|
80
|
+
return () => {
|
|
81
|
+
client.off('update', evaluate);
|
|
82
|
+
client.off('ready', evaluate);
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|