flipflag-sdk 1.1.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 +713 -0
- package/dist/core/FlipFlagSDK.js +379 -0
- package/dist/index.js +384 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +382 -0
- package/dist/index.mjs.map +1 -0
- package/dist/js/featureFlagManager.js +172 -0
- package/dist/js/identifyUser.js +15 -0
- package/dist/js/index.js +3 -0
- package/dist/js.js +565 -0
- package/dist/js.js.map +1 -0
- package/dist/js.mjs +561 -0
- package/dist/js.mjs.map +1 -0
- package/dist/next/FlipflagProvider.js +27 -0
- package/dist/next/identifyUser.js +9 -0
- package/dist/next/index.js +6 -0
- package/dist/next/server-utils.js +70 -0
- package/dist/next/types.js +1 -0
- package/dist/next/useABTest.js +53 -0
- package/dist/next/useFeatureFlag.js +55 -0
- package/dist/next/useFeatureFlags.js +69 -0
- package/dist/next.js +657 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +650 -0
- package/dist/next.mjs.map +1 -0
- package/dist/react/FeatureFlagProvider.js +19 -0
- package/dist/react/identifyUser.js +9 -0
- package/dist/react/index.js +5 -0
- package/dist/react/types.js +1 -0
- package/dist/react/useABTest.js +64 -0
- package/dist/react/useFeatureFlag.js +84 -0
- package/dist/react/useFeatureFlags.js +74 -0
- package/dist/react-native/FeatureFlagProvider.js +19 -0
- package/dist/react-native/identifyUser.js +9 -0
- package/dist/react-native/index.js +8 -0
- package/dist/react-native/offlineStorage.js +39 -0
- package/dist/react-native/types.js +1 -0
- package/dist/react-native/useABTest.js +87 -0
- package/dist/react-native/useFeatureFlag.js +82 -0
- package/dist/react-native/useFeatureFlags.js +115 -0
- package/dist/react-native.js +732 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react-native.mjs +723 -0
- package/dist/react-native.mjs.map +1 -0
- package/dist/react.js +634 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +627 -0
- package/dist/react.mjs.map +1 -0
- package/dist/types/core/FlipFlagSDK.d.ts +94 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -0
- package/dist/types/js/featureFlagManager.d.ts +59 -0
- package/dist/types/js/identifyUser.d.ts +11 -0
- package/dist/types/js/index.d.ts +4 -0
- package/dist/types/next/FlipflagProvider.d.ts +18 -0
- package/dist/types/next/identifyUser.d.ts +6 -0
- package/dist/types/next/index.d.ts +7 -0
- package/dist/types/next/server-utils.d.ts +15 -0
- package/dist/types/next/types.d.ts +24 -0
- package/dist/types/next/useABTest.d.ts +10 -0
- package/dist/types/next/useFeatureFlag.d.ts +10 -0
- package/dist/types/next/useFeatureFlags.d.ts +10 -0
- package/dist/types/react/FeatureFlagProvider.d.ts +14 -0
- package/dist/types/react/identifyUser.d.ts +6 -0
- package/dist/types/react/index.d.ts +6 -0
- package/dist/types/react/types.d.ts +20 -0
- package/dist/types/react/useABTest.d.ts +10 -0
- package/dist/types/react/useFeatureFlag.d.ts +10 -0
- package/dist/types/react/useFeatureFlags.d.ts +10 -0
- package/dist/types/react-native/FeatureFlagProvider.d.ts +14 -0
- package/dist/types/react-native/identifyUser.d.ts +6 -0
- package/dist/types/react-native/index.d.ts +9 -0
- package/dist/types/react-native/offlineStorage.d.ts +9 -0
- package/dist/types/react-native/types.d.ts +25 -0
- package/dist/types/react-native/useABTest.d.ts +10 -0
- package/dist/types/react-native/useFeatureFlag.d.ts +10 -0
- package/dist/types/react-native/useFeatureFlags.d.ts +10 -0
- package/dist/types/types/index.d.ts +86 -0
- package/dist/types/vue/identifyUser.d.ts +6 -0
- package/dist/types/vue/index.d.ts +8 -0
- package/dist/types/vue/plugin.d.ts +19 -0
- package/dist/types/vue/types.d.ts +20 -0
- package/dist/types/vue/useABTest.d.ts +10 -0
- package/dist/types/vue/useFeatureFlag.d.ts +10 -0
- package/dist/types/vue/useFeatureFlags.d.ts +10 -0
- package/dist/vue/identifyUser.js +13 -0
- package/dist/vue/index.js +7 -0
- package/dist/vue/plugin.js +30 -0
- package/dist/vue/types.js +1 -0
- package/dist/vue/useABTest.js +93 -0
- package/dist/vue/useFeatureFlag.js +87 -0
- package/dist/vue/useFeatureFlags.js +120 -0
- package/dist/vue.js +718 -0
- package/dist/vue.js.map +1 -0
- package/dist/vue.mjs +710 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +125 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
class FlipFlagSDK {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.cache = new Map();
|
|
4
|
+
this.defaultBaseUrl = "https://app.flipflag.ru/api";
|
|
5
|
+
this.stateListeners = new Set();
|
|
6
|
+
this.config = {
|
|
7
|
+
baseUrl: this.defaultBaseUrl,
|
|
8
|
+
cacheTimeout: 30000, // 30 seconds
|
|
9
|
+
retryAttempts: 3,
|
|
10
|
+
retryDelay: 1000, // 1 second
|
|
11
|
+
pullInterval: 60000, // 1 minute
|
|
12
|
+
...config,
|
|
13
|
+
};
|
|
14
|
+
this.state = {
|
|
15
|
+
flags: {},
|
|
16
|
+
abTests: {},
|
|
17
|
+
lastFetch: null,
|
|
18
|
+
isLoading: false,
|
|
19
|
+
error: null,
|
|
20
|
+
};
|
|
21
|
+
// Start automatic pulling if pullInterval is set
|
|
22
|
+
if (this.config.pullInterval && this.config.pullInterval > 0) {
|
|
23
|
+
this.startPulling();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Identify user for A/B testing and user-specific flags
|
|
28
|
+
*/
|
|
29
|
+
identifyUser(options) {
|
|
30
|
+
this.config.userId = options.userId;
|
|
31
|
+
// Clear A/B test cache since user changed
|
|
32
|
+
this.clearABTestCache();
|
|
33
|
+
// If we have a user, we might want to refresh immediately
|
|
34
|
+
if (this.config.pullInterval && this.config.pullInterval > 0) {
|
|
35
|
+
this.pullData();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get current SDK state
|
|
40
|
+
*/
|
|
41
|
+
getState() {
|
|
42
|
+
return { ...this.state };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Subscribe to state changes
|
|
46
|
+
*/
|
|
47
|
+
subscribe(callback) {
|
|
48
|
+
this.stateListeners.add(callback);
|
|
49
|
+
// Return unsubscribe function
|
|
50
|
+
return () => {
|
|
51
|
+
this.stateListeners.delete(callback);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get a feature flag value from current state
|
|
56
|
+
*/
|
|
57
|
+
getFlagValue(flagName) {
|
|
58
|
+
var _a;
|
|
59
|
+
return (_a = this.state.flags[flagName]) !== null && _a !== void 0 ? _a : false;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get A/B test variant from current state
|
|
63
|
+
*/
|
|
64
|
+
getABTestVariant(testName) {
|
|
65
|
+
return this.state.abTests[testName] || null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Start automatic data pulling
|
|
69
|
+
*/
|
|
70
|
+
startPulling() {
|
|
71
|
+
if (this.pullIntervalId) {
|
|
72
|
+
clearInterval(this.pullIntervalId);
|
|
73
|
+
}
|
|
74
|
+
this.pullIntervalId = setInterval(() => {
|
|
75
|
+
this.pullData();
|
|
76
|
+
}, this.config.pullInterval);
|
|
77
|
+
// Initial pull
|
|
78
|
+
this.pullData();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stop automatic data pulling
|
|
82
|
+
*/
|
|
83
|
+
stopPulling() {
|
|
84
|
+
if (this.pullIntervalId) {
|
|
85
|
+
clearInterval(this.pullIntervalId);
|
|
86
|
+
this.pullIntervalId = undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Pull fresh data from API
|
|
91
|
+
*/
|
|
92
|
+
async pullData() {
|
|
93
|
+
try {
|
|
94
|
+
this.updateState({ isLoading: true, error: null });
|
|
95
|
+
// Pull flags
|
|
96
|
+
const flagsResponse = await this.fetchAllFlags();
|
|
97
|
+
// Pull A/B tests if user is identified
|
|
98
|
+
let abTestsResponse = null;
|
|
99
|
+
if (this.config.userId) {
|
|
100
|
+
try {
|
|
101
|
+
abTestsResponse = await this.fetchAllABTests();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.warn("Failed to fetch A/B tests:", error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const newState = {
|
|
108
|
+
flags: (flagsResponse === null || flagsResponse === void 0 ? void 0 : flagsResponse.flags) || {},
|
|
109
|
+
lastFetch: new Date(),
|
|
110
|
+
isLoading: false,
|
|
111
|
+
error: null,
|
|
112
|
+
};
|
|
113
|
+
if (abTestsResponse) {
|
|
114
|
+
const abTestsMap = {};
|
|
115
|
+
abTestsResponse.abTests.forEach((test) => {
|
|
116
|
+
// For each test, get the variant for current user
|
|
117
|
+
if (this.config.userId) {
|
|
118
|
+
// This would normally fetch the variant, but for demo we'll simulate
|
|
119
|
+
abTestsMap[test.name] = {
|
|
120
|
+
testName: test.name,
|
|
121
|
+
testId: test.id,
|
|
122
|
+
variantId: "variant_a", // Default variant
|
|
123
|
+
variant: test.variants[0] || {
|
|
124
|
+
id: "default",
|
|
125
|
+
name: "Default",
|
|
126
|
+
value: null,
|
|
127
|
+
weight: 100,
|
|
128
|
+
},
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
newState.abTests = abTestsMap;
|
|
134
|
+
}
|
|
135
|
+
this.updateState(newState);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
139
|
+
this.updateState({
|
|
140
|
+
isLoading: false,
|
|
141
|
+
error: errorMessage,
|
|
142
|
+
});
|
|
143
|
+
console.error("Failed to pull data:", error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Update state and notify listeners
|
|
148
|
+
*/
|
|
149
|
+
updateState(newState) {
|
|
150
|
+
this.state = { ...this.state, ...newState };
|
|
151
|
+
// Notify all listeners
|
|
152
|
+
this.stateListeners.forEach((callback) => {
|
|
153
|
+
try {
|
|
154
|
+
callback(this.state);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error("State listener error:", error);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Fetch all flags from API
|
|
163
|
+
*/
|
|
164
|
+
async fetchAllFlags() {
|
|
165
|
+
try {
|
|
166
|
+
return await this.makeRequest(`/sdk/flags/${this.config.projectId}`);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.error("Failed to fetch flags:", error);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Fetch all A/B tests from API
|
|
175
|
+
*/
|
|
176
|
+
async fetchAllABTests() {
|
|
177
|
+
try {
|
|
178
|
+
return await this.makeRequest(`/sdk/ab-tests/${this.config.projectId}`);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error("Failed to fetch A/B tests:", error);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Clear A/B test cache
|
|
187
|
+
*/
|
|
188
|
+
clearABTestCache() {
|
|
189
|
+
const abTestKeys = Array.from(this.cache.keys()).filter((key) => key.startsWith("abtest:"));
|
|
190
|
+
abTestKeys.forEach((key) => this.cache.delete(key));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get all feature flags for a project
|
|
194
|
+
*/
|
|
195
|
+
async getFlags(projectId = this.config.projectId, environment) {
|
|
196
|
+
const cacheKey = `flags:${projectId}:${environment || "all"}`;
|
|
197
|
+
const cached = this.getFromCache(cacheKey);
|
|
198
|
+
if (cached) {
|
|
199
|
+
return cached;
|
|
200
|
+
}
|
|
201
|
+
const url = new URL(`/sdk/flags/${projectId}`, this.config.baseUrl);
|
|
202
|
+
if (environment || this.config.environment) {
|
|
203
|
+
url.searchParams.set("environment", environment || this.config.environment);
|
|
204
|
+
}
|
|
205
|
+
const response = await this.makeRequest(url.toString());
|
|
206
|
+
this.setCache(cacheKey, response, this.config.cacheTimeout);
|
|
207
|
+
return response;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get a specific feature flag value
|
|
211
|
+
*/
|
|
212
|
+
async getFlag(flagName, projectId = this.config.projectId, environment) {
|
|
213
|
+
var _a;
|
|
214
|
+
// If we have the flag in state, return it immediately
|
|
215
|
+
if (this.state.flags[flagName] !== undefined) {
|
|
216
|
+
return {
|
|
217
|
+
projectId: this.config.projectId,
|
|
218
|
+
flagName,
|
|
219
|
+
environment: this.config.environment || "all",
|
|
220
|
+
value: this.state.flags[flagName],
|
|
221
|
+
timestamp: ((_a = this.state.lastFetch) === null || _a === void 0 ? void 0 : _a.toISOString()) || new Date().toISOString(),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const cacheKey = `flag:${projectId}:${flagName}:${environment || "all"}`;
|
|
225
|
+
const cached = this.getFromCache(cacheKey);
|
|
226
|
+
if (cached) {
|
|
227
|
+
return cached;
|
|
228
|
+
}
|
|
229
|
+
const url = new URL(`/sdk/flags/${projectId}/${flagName}`, this.config.baseUrl);
|
|
230
|
+
if (environment || this.config.environment) {
|
|
231
|
+
url.searchParams.set("environment", environment || this.config.environment);
|
|
232
|
+
}
|
|
233
|
+
const response = await this.makeRequest(url.toString());
|
|
234
|
+
this.setCache(cacheKey, response, this.config.cacheTimeout);
|
|
235
|
+
return response;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get A/B test variant for a user
|
|
239
|
+
*/
|
|
240
|
+
async getAbTestVariant(testName, userId, projectId = this.config.projectId) {
|
|
241
|
+
// If we have the A/B test in state, return it immediately
|
|
242
|
+
const stateVariant = this.state.abTests[testName];
|
|
243
|
+
if (stateVariant && stateVariant.testName === testName) {
|
|
244
|
+
return stateVariant;
|
|
245
|
+
}
|
|
246
|
+
const cacheKey = `abtest:${projectId}:${testName}:${userId}`;
|
|
247
|
+
const cached = this.getFromCache(cacheKey);
|
|
248
|
+
if (cached) {
|
|
249
|
+
return cached;
|
|
250
|
+
}
|
|
251
|
+
const url = `${this.config.baseUrl}/sdk/ab-test/${projectId}/${testName}`;
|
|
252
|
+
const response = await this.makeRequest(url, {
|
|
253
|
+
headers: {
|
|
254
|
+
"X-User-ID": userId,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
// Cache A/B test variants for longer since they shouldn't change frequently for a user
|
|
258
|
+
this.setCache(cacheKey, response, this.config.cacheTimeout * 10);
|
|
259
|
+
return response;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Record an A/B test event
|
|
263
|
+
*/
|
|
264
|
+
async recordAbTestEvent(testName, userId, event, variantId, eventData, projectId = this.config.projectId) {
|
|
265
|
+
const url = `${this.config.baseUrl}/sdk/ab-test/${projectId}/${testName}/event`;
|
|
266
|
+
return this.makeRequest(url, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: {
|
|
269
|
+
"X-User-ID": userId,
|
|
270
|
+
"Content-Type": "application/json",
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
event,
|
|
274
|
+
variantId,
|
|
275
|
+
eventData,
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get all A/B tests for a project
|
|
281
|
+
*/
|
|
282
|
+
async getAbTests(projectId = this.config.projectId) {
|
|
283
|
+
const cacheKey = `abtests:${projectId}`;
|
|
284
|
+
const cached = this.getFromCache(cacheKey);
|
|
285
|
+
if (cached) {
|
|
286
|
+
return cached;
|
|
287
|
+
}
|
|
288
|
+
const url = `${this.config.baseUrl}/sdk/ab-tests/${projectId}`;
|
|
289
|
+
const response = await this.makeRequest(url);
|
|
290
|
+
this.setCache(cacheKey, response, this.config.cacheTimeout);
|
|
291
|
+
return response;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Clear cache for specific key or all cache
|
|
295
|
+
*/
|
|
296
|
+
clearCache(key) {
|
|
297
|
+
if (key) {
|
|
298
|
+
this.cache.delete(key);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.cache.clear();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Update configuration
|
|
306
|
+
*/
|
|
307
|
+
updateConfig(newConfig) {
|
|
308
|
+
this.config = { ...this.config, ...newConfig };
|
|
309
|
+
}
|
|
310
|
+
async makeRequest(url, options = {}) {
|
|
311
|
+
const headers = {
|
|
312
|
+
"X-API-Key": this.config.apiKey,
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
...options.headers,
|
|
315
|
+
};
|
|
316
|
+
const requestOptions = {
|
|
317
|
+
...options,
|
|
318
|
+
headers,
|
|
319
|
+
};
|
|
320
|
+
return this.makeRequestWithRetry(url, requestOptions);
|
|
321
|
+
}
|
|
322
|
+
async makeRequestWithRetry(url, options, retryOptions = {
|
|
323
|
+
attempts: this.config.retryAttempts,
|
|
324
|
+
delay: this.config.retryDelay,
|
|
325
|
+
backoff: 2,
|
|
326
|
+
}) {
|
|
327
|
+
let lastError;
|
|
328
|
+
for (let attempt = 1; attempt <= retryOptions.attempts; attempt++) {
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch(url, options);
|
|
331
|
+
if (!response.ok) {
|
|
332
|
+
const errorData = await response.json().catch(() => ({}));
|
|
333
|
+
throw new Error(`HTTP ${response.status}: ${errorData.error || response.statusText}`);
|
|
334
|
+
}
|
|
335
|
+
return await response.json();
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
lastError = error;
|
|
339
|
+
// Don't retry on client errors (4xx) except 429 (rate limit)
|
|
340
|
+
if (error instanceof Error && error.message.includes("HTTP 4")) {
|
|
341
|
+
if (!error.message.includes("HTTP 429")) {
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (attempt < retryOptions.attempts) {
|
|
346
|
+
const delay = retryOptions.delay * Math.pow(retryOptions.backoff, attempt - 1);
|
|
347
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
throw lastError;
|
|
352
|
+
}
|
|
353
|
+
getFromCache(key) {
|
|
354
|
+
const entry = this.cache.get(key);
|
|
355
|
+
if (!entry) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
359
|
+
this.cache.delete(key);
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return entry.data;
|
|
363
|
+
}
|
|
364
|
+
setCache(key, data, ttl) {
|
|
365
|
+
this.cache.set(key, {
|
|
366
|
+
data,
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
ttl,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Cleanup method - should be called when SDK is no longer needed
|
|
373
|
+
*/
|
|
374
|
+
destroy() {
|
|
375
|
+
this.stopPulling();
|
|
376
|
+
this.stateListeners.clear();
|
|
377
|
+
this.cache.clear();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { FlipFlagSDK };
|
|
382
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/core/FlipFlagSDK.ts"],"sourcesContent":["import {\n FlipFlagConfig,\n FeatureFlagResponse,\n FeatureFlagsResponse,\n ABTestResponse,\n ABTestEventResponse,\n ABTestsResponse,\n FlipFlagError,\n CacheEntry,\n RetryOptions,\n SDKState,\n IdentifyUserOptions,\n} from \"../types\";\n\nexport class FlipFlagSDK {\n private config: FlipFlagConfig;\n private cache = new Map<string, CacheEntry<any>>();\n private defaultBaseUrl = \"https://app.flipflag.ru/api\";\n private pullIntervalId?: NodeJS.Timeout;\n private state: SDKState;\n private stateListeners: Set<(state: SDKState) => void> = new Set();\n\n constructor(config: FlipFlagConfig) {\n this.config = {\n baseUrl: this.defaultBaseUrl,\n cacheTimeout: 30000, // 30 seconds\n retryAttempts: 3,\n retryDelay: 1000, // 1 second\n pullInterval: 60000, // 1 minute\n ...config,\n };\n\n this.state = {\n flags: {},\n abTests: {},\n lastFetch: null,\n isLoading: false,\n error: null,\n };\n\n // Start automatic pulling if pullInterval is set\n if (this.config.pullInterval && this.config.pullInterval > 0) {\n this.startPulling();\n }\n }\n\n /**\n * Identify user for A/B testing and user-specific flags\n */\n identifyUser(options: IdentifyUserOptions): void {\n this.config.userId = options.userId;\n\n // Clear A/B test cache since user changed\n this.clearABTestCache();\n\n // If we have a user, we might want to refresh immediately\n if (this.config.pullInterval && this.config.pullInterval > 0) {\n this.pullData();\n }\n }\n\n /**\n * Get current SDK state\n */\n getState(): SDKState {\n return { ...this.state };\n }\n\n /**\n * Subscribe to state changes\n */\n subscribe(callback: (state: SDKState) => void): () => void {\n this.stateListeners.add(callback);\n\n // Return unsubscribe function\n return () => {\n this.stateListeners.delete(callback);\n };\n }\n\n /**\n * Get a feature flag value from current state\n */\n getFlagValue(flagName: string): boolean {\n return this.state.flags[flagName] ?? false;\n }\n\n /**\n * Get A/B test variant from current state\n */\n getABTestVariant(testName: string): ABTestResponse | null {\n return this.state.abTests[testName] || null;\n }\n\n /**\n * Start automatic data pulling\n */\n private startPulling(): void {\n if (this.pullIntervalId) {\n clearInterval(this.pullIntervalId);\n }\n\n this.pullIntervalId = setInterval(() => {\n this.pullData();\n }, this.config.pullInterval!);\n\n // Initial pull\n this.pullData();\n }\n\n /**\n * Stop automatic data pulling\n */\n stopPulling(): void {\n if (this.pullIntervalId) {\n clearInterval(this.pullIntervalId);\n this.pullIntervalId = undefined;\n }\n }\n\n /**\n * Pull fresh data from API\n */\n private async pullData(): Promise<void> {\n try {\n this.updateState({ isLoading: true, error: null });\n\n // Pull flags\n const flagsResponse = await this.fetchAllFlags();\n\n // Pull A/B tests if user is identified\n let abTestsResponse: ABTestsResponse | null = null;\n if (this.config.userId) {\n try {\n abTestsResponse = await this.fetchAllABTests();\n } catch (error) {\n console.warn(\"Failed to fetch A/B tests:\", error);\n }\n }\n\n const newState: Partial<SDKState> = {\n flags: flagsResponse?.flags || {},\n lastFetch: new Date(),\n isLoading: false,\n error: null,\n };\n\n if (abTestsResponse) {\n const abTestsMap: Record<string, ABTestResponse> = {};\n abTestsResponse.abTests.forEach((test) => {\n // For each test, get the variant for current user\n if (this.config.userId) {\n // This would normally fetch the variant, but for demo we'll simulate\n abTestsMap[test.name] = {\n testName: test.name,\n testId: test.id,\n variantId: \"variant_a\", // Default variant\n variant: test.variants[0] || {\n id: \"default\",\n name: \"Default\",\n value: null,\n weight: 100,\n },\n timestamp: new Date().toISOString(),\n };\n }\n });\n newState.abTests = abTestsMap;\n }\n\n this.updateState(newState);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : \"Unknown error\";\n this.updateState({\n isLoading: false,\n error: errorMessage,\n });\n console.error(\"Failed to pull data:\", error);\n }\n }\n\n /**\n * Update state and notify listeners\n */\n private updateState(newState: Partial<SDKState>): void {\n this.state = { ...this.state, ...newState };\n\n // Notify all listeners\n this.stateListeners.forEach((callback) => {\n try {\n callback(this.state);\n } catch (error) {\n console.error(\"State listener error:\", error);\n }\n });\n }\n\n /**\n * Fetch all flags from API\n */\n private async fetchAllFlags(): Promise<FeatureFlagsResponse | null> {\n try {\n return await this.makeRequest<FeatureFlagsResponse>(\n `/sdk/flags/${this.config.projectId}`\n );\n } catch (error) {\n console.error(\"Failed to fetch flags:\", error);\n return null;\n }\n }\n\n /**\n * Fetch all A/B tests from API\n */\n private async fetchAllABTests(): Promise<ABTestsResponse | null> {\n try {\n return await this.makeRequest<ABTestsResponse>(\n `/sdk/ab-tests/${this.config.projectId}`\n );\n } catch (error) {\n console.error(\"Failed to fetch A/B tests:\", error);\n return null;\n }\n }\n\n /**\n * Clear A/B test cache\n */\n private clearABTestCache(): void {\n const abTestKeys = Array.from(this.cache.keys()).filter((key) =>\n key.startsWith(\"abtest:\")\n );\n abTestKeys.forEach((key) => this.cache.delete(key));\n }\n\n /**\n * Get all feature flags for a project\n */\n async getFlags(\n projectId: string = this.config.projectId,\n environment?: string\n ): Promise<FeatureFlagsResponse> {\n const cacheKey = `flags:${projectId}:${environment || \"all\"}`;\n const cached = this.getFromCache<FeatureFlagsResponse>(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const url = new URL(`/sdk/flags/${projectId}`, this.config.baseUrl);\n if (environment || this.config.environment) {\n url.searchParams.set(\n \"environment\",\n environment || this.config.environment!\n );\n }\n\n const response = await this.makeRequest<FeatureFlagsResponse>(\n url.toString()\n );\n this.setCache(cacheKey, response, this.config.cacheTimeout!);\n\n return response;\n }\n\n /**\n * Get a specific feature flag value\n */\n async getFlag(\n flagName: string,\n projectId: string = this.config.projectId,\n environment?: string\n ): Promise<FeatureFlagResponse> {\n // If we have the flag in state, return it immediately\n if (this.state.flags[flagName] !== undefined) {\n return {\n projectId: this.config.projectId,\n flagName,\n environment: this.config.environment || \"all\",\n value: this.state.flags[flagName],\n timestamp:\n this.state.lastFetch?.toISOString() || new Date().toISOString(),\n };\n }\n\n const cacheKey = `flag:${projectId}:${flagName}:${environment || \"all\"}`;\n const cached = this.getFromCache<FeatureFlagResponse>(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const url = new URL(\n `/sdk/flags/${projectId}/${flagName}`,\n this.config.baseUrl\n );\n if (environment || this.config.environment) {\n url.searchParams.set(\n \"environment\",\n environment || this.config.environment!\n );\n }\n\n const response = await this.makeRequest<FeatureFlagResponse>(\n url.toString()\n );\n this.setCache(cacheKey, response, this.config.cacheTimeout!);\n\n return response;\n }\n\n /**\n * Get A/B test variant for a user\n */\n async getAbTestVariant(\n testName: string,\n userId: string,\n projectId: string = this.config.projectId\n ): Promise<ABTestResponse> {\n // If we have the A/B test in state, return it immediately\n const stateVariant = this.state.abTests[testName];\n if (stateVariant && stateVariant.testName === testName) {\n return stateVariant;\n }\n\n const cacheKey = `abtest:${projectId}:${testName}:${userId}`;\n const cached = this.getFromCache<ABTestResponse>(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const url = `${this.config.baseUrl}/sdk/ab-test/${projectId}/${testName}`;\n\n const response = await this.makeRequest<ABTestResponse>(url, {\n headers: {\n \"X-User-ID\": userId,\n },\n });\n\n // Cache A/B test variants for longer since they shouldn't change frequently for a user\n this.setCache(cacheKey, response, this.config.cacheTimeout! * 10);\n\n return response;\n }\n\n /**\n * Record an A/B test event\n */\n async recordAbTestEvent(\n testName: string,\n userId: string,\n event: string,\n variantId: string,\n eventData?: Record<string, any>,\n projectId: string = this.config.projectId\n ): Promise<ABTestEventResponse> {\n const url = `${this.config.baseUrl}/sdk/ab-test/${projectId}/${testName}/event`;\n\n return this.makeRequest<ABTestEventResponse>(url, {\n method: \"POST\",\n headers: {\n \"X-User-ID\": userId,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n event,\n variantId,\n eventData,\n }),\n });\n }\n\n /**\n * Get all A/B tests for a project\n */\n async getAbTests(\n projectId: string = this.config.projectId\n ): Promise<ABTestsResponse> {\n const cacheKey = `abtests:${projectId}`;\n const cached = this.getFromCache<ABTestsResponse>(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const url = `${this.config.baseUrl}/sdk/ab-tests/${projectId}`;\n\n const response = await this.makeRequest<ABTestsResponse>(url);\n this.setCache(cacheKey, response, this.config.cacheTimeout!);\n\n return response;\n }\n\n /**\n * Clear cache for specific key or all cache\n */\n clearCache(key?: string): void {\n if (key) {\n this.cache.delete(key);\n } else {\n this.cache.clear();\n }\n }\n\n /**\n * Update configuration\n */\n updateConfig(newConfig: Partial<FlipFlagConfig>): void {\n this.config = { ...this.config, ...newConfig };\n }\n\n private async makeRequest<T>(\n url: string,\n options: RequestInit = {}\n ): Promise<T> {\n const headers = {\n \"X-API-Key\": this.config.apiKey,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n };\n\n const requestOptions: RequestInit = {\n ...options,\n headers,\n };\n\n return this.makeRequestWithRetry<T>(url, requestOptions);\n }\n\n private async makeRequestWithRetry<T>(\n url: string,\n options: RequestInit,\n retryOptions: RetryOptions = {\n attempts: this.config.retryAttempts!,\n delay: this.config.retryDelay!,\n backoff: 2,\n }\n ): Promise<T> {\n let lastError: Error;\n\n for (let attempt = 1; attempt <= retryOptions.attempts; attempt++) {\n try {\n const response = await fetch(url, options);\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(\n `HTTP ${response.status}: ${errorData.error || response.statusText}`\n );\n }\n\n return await response.json();\n } catch (error) {\n lastError = error as Error;\n\n // Don't retry on client errors (4xx) except 429 (rate limit)\n if (error instanceof Error && error.message.includes(\"HTTP 4\")) {\n if (!error.message.includes(\"HTTP 429\")) {\n throw error;\n }\n }\n\n if (attempt < retryOptions.attempts) {\n const delay =\n retryOptions.delay * Math.pow(retryOptions.backoff, attempt - 1);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw lastError!;\n }\n\n private getFromCache<T>(key: string): T | null {\n const entry = this.cache.get(key);\n\n if (!entry) {\n return null;\n }\n\n if (Date.now() - entry.timestamp > entry.ttl) {\n this.cache.delete(key);\n return null;\n }\n\n return entry.data;\n }\n\n private setCache<T>(key: string, data: T, ttl: number): void {\n this.cache.set(key, {\n data,\n timestamp: Date.now(),\n ttl,\n });\n }\n\n /**\n * Cleanup method - should be called when SDK is no longer needed\n */\n public destroy(): void {\n this.stopPulling();\n this.stateListeners.clear();\n this.cache.clear();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { FlipFlagSDK } from "../core/FlipFlagSDK";
|
|
2
|
+
export class FeatureFlagManager {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this.flags = new Map();
|
|
5
|
+
this.abTests = new Map();
|
|
6
|
+
this.listeners = new Map();
|
|
7
|
+
this.sdk = new FlipFlagSDK(options.config);
|
|
8
|
+
if (options.autoRefresh) {
|
|
9
|
+
this.startAutoRefresh(options.refreshInterval || 30000);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get a feature flag value
|
|
14
|
+
*/
|
|
15
|
+
async getFlag(flagName) {
|
|
16
|
+
var _a;
|
|
17
|
+
try {
|
|
18
|
+
const response = await this.sdk.getFlag(flagName);
|
|
19
|
+
const value = response.value;
|
|
20
|
+
this.flags.set(flagName, value);
|
|
21
|
+
this.notifyListeners(flagName, value);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(`Failed to get flag ${flagName}:`, error);
|
|
26
|
+
return (_a = this.flags.get(flagName)) !== null && _a !== void 0 ? _a : false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get multiple feature flags
|
|
31
|
+
*/
|
|
32
|
+
async getFlags(flagNames) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await this.sdk.getFlags();
|
|
35
|
+
const result = {};
|
|
36
|
+
flagNames.forEach((flagName) => {
|
|
37
|
+
var _a;
|
|
38
|
+
const value = (_a = response.flags[flagName]) !== null && _a !== void 0 ? _a : false;
|
|
39
|
+
result[flagName] = value;
|
|
40
|
+
this.flags.set(flagName, value);
|
|
41
|
+
this.notifyListeners(flagName, value);
|
|
42
|
+
});
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error("Failed to get flags:", error);
|
|
47
|
+
const result = {};
|
|
48
|
+
flagNames.forEach((flagName) => {
|
|
49
|
+
var _a;
|
|
50
|
+
result[flagName] = (_a = this.flags.get(flagName)) !== null && _a !== void 0 ? _a : false;
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get A/B test variant
|
|
57
|
+
*/
|
|
58
|
+
async getABTestVariant(testName, userId) {
|
|
59
|
+
var _a;
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.sdk.getAbTestVariant(testName, userId);
|
|
62
|
+
const result = {
|
|
63
|
+
variant: response.variant.value,
|
|
64
|
+
variantId: response.variantId,
|
|
65
|
+
};
|
|
66
|
+
this.abTests.set(`${testName}:${userId}`, result);
|
|
67
|
+
this.notifyListeners(`${testName}:${userId}`, result);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(`Failed to get A/B test variant for ${testName}:`, error);
|
|
72
|
+
return ((_a = this.abTests.get(`${testName}:${userId}`)) !== null && _a !== void 0 ? _a : {
|
|
73
|
+
variant: null,
|
|
74
|
+
variantId: "",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Record A/B test event
|
|
80
|
+
*/
|
|
81
|
+
async recordABTestEvent(testName, userId, event, eventData) {
|
|
82
|
+
const testKey = `${testName}:${userId}`;
|
|
83
|
+
const testData = this.abTests.get(testKey);
|
|
84
|
+
if (!testData) {
|
|
85
|
+
console.warn(`No A/B test data found for ${testKey}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await this.sdk.recordAbTestEvent(testName, userId, event, testData.variantId, eventData);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error(`Failed to record A/B test event for ${testName}:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Subscribe to flag changes
|
|
97
|
+
*/
|
|
98
|
+
onFlagChange(flagName, callback) {
|
|
99
|
+
if (!this.listeners.has(flagName)) {
|
|
100
|
+
this.listeners.set(flagName, new Set());
|
|
101
|
+
}
|
|
102
|
+
this.listeners.get(flagName).add(callback);
|
|
103
|
+
// Return unsubscribe function
|
|
104
|
+
return () => {
|
|
105
|
+
const listeners = this.listeners.get(flagName);
|
|
106
|
+
if (listeners) {
|
|
107
|
+
listeners.delete(callback);
|
|
108
|
+
if (listeners.size === 0) {
|
|
109
|
+
this.listeners.delete(flagName);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get cached flag value
|
|
116
|
+
*/
|
|
117
|
+
getCachedFlag(flagName) {
|
|
118
|
+
return this.flags.get(flagName);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clear cache
|
|
122
|
+
*/
|
|
123
|
+
clearCache() {
|
|
124
|
+
this.sdk.clearCache();
|
|
125
|
+
this.flags.clear();
|
|
126
|
+
this.abTests.clear();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Start auto-refresh
|
|
130
|
+
*/
|
|
131
|
+
startAutoRefresh(interval) {
|
|
132
|
+
this.refreshTimer = setInterval(async () => {
|
|
133
|
+
try {
|
|
134
|
+
// Refresh all cached flags
|
|
135
|
+
for (const [flagName] of this.flags) {
|
|
136
|
+
await this.getFlag(flagName);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error("Auto-refresh failed:", error);
|
|
141
|
+
}
|
|
142
|
+
}, interval);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Stop auto-refresh
|
|
146
|
+
*/
|
|
147
|
+
stopAutoRefresh() {
|
|
148
|
+
if (this.refreshTimer) {
|
|
149
|
+
clearInterval(this.refreshTimer);
|
|
150
|
+
this.refreshTimer = undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
notifyListeners(key, value) {
|
|
154
|
+
const listeners = this.listeners.get(key);
|
|
155
|
+
if (listeners) {
|
|
156
|
+
listeners.forEach((callback) => {
|
|
157
|
+
try {
|
|
158
|
+
callback(value);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error("Flag change listener error:", error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Create a feature flag manager instance
|
|
169
|
+
*/
|
|
170
|
+
export function createFeatureFlagManager(options) {
|
|
171
|
+
return new FeatureFlagManager(options);
|
|
172
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identify user for A/B testing and user-specific flags
|
|
3
|
+
*/
|
|
4
|
+
export function identifyUser(sdk, options) {
|
|
5
|
+
sdk.identifyUser(options);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Global identifyUser function that works with the default SDK instance
|
|
9
|
+
* Note: This requires a global SDK instance to be set up
|
|
10
|
+
*/
|
|
11
|
+
export function identifyUserGlobal(options) {
|
|
12
|
+
// This would need to be implemented with a global registry
|
|
13
|
+
// For now, throw an error
|
|
14
|
+
throw new Error("Global identifyUser requires a global SDK instance. Use identifyUser(sdk, options) instead.");
|
|
15
|
+
}
|
package/dist/js/index.js
ADDED