@flagix/js-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/.turbo/turbo-build.log +19 -0
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +561 -0
- package/dist/index.mjs +524 -0
- package/package.json +46 -0
- package/src/client.ts +482 -0
- package/src/index.ts +139 -0
- package/src/lib/constants.ts +12 -0
- package/src/lib/emitter.ts +10 -0
- package/src/lib/logger.ts +41 -0
- package/src/sse/create-event-source.ts +33 -0
- package/src/types.ts +22 -0
- package/tsconfig.json +21 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { evaluateFlag, resolveIdentifier } from "@flagix/evaluation-core";
|
|
2
|
+
import {
|
|
3
|
+
EVENT_TO_LISTEN,
|
|
4
|
+
type FlagUpdateType,
|
|
5
|
+
REMOVE_TRAILING_SLASH,
|
|
6
|
+
} from "@/lib/constants";
|
|
7
|
+
import { FLAG_UPDATE_EVENT, FlagixEventEmitter } from "@/lib/emitter";
|
|
8
|
+
import { log, setLogLevel } from "@/lib/logger";
|
|
9
|
+
import {
|
|
10
|
+
createEventSource,
|
|
11
|
+
type FlagStreamConnection,
|
|
12
|
+
} from "@/sse/create-event-source";
|
|
13
|
+
import type {
|
|
14
|
+
EvaluationContext,
|
|
15
|
+
FlagConfig,
|
|
16
|
+
FlagixClientOptions,
|
|
17
|
+
FlagVariation,
|
|
18
|
+
VariationValue,
|
|
19
|
+
} from "@/types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The primary class for the Flagix SDK. Manages configuration state,
|
|
23
|
+
* local caching, and evaluation.
|
|
24
|
+
*/
|
|
25
|
+
export class FlagixClient {
|
|
26
|
+
private readonly apiKey: string;
|
|
27
|
+
private readonly apiBaseUrl: string;
|
|
28
|
+
private readonly localCache = new Map<string, FlagConfig>();
|
|
29
|
+
private context: EvaluationContext;
|
|
30
|
+
private isInitialized = false;
|
|
31
|
+
private sseConnection: FlagStreamConnection | null = null;
|
|
32
|
+
private readonly emitter: FlagixEventEmitter;
|
|
33
|
+
private reconnectAttempts = 0;
|
|
34
|
+
private reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
35
|
+
private isReconnecting = false;
|
|
36
|
+
private hasEstablishedConnection = false;
|
|
37
|
+
private readonly maxReconnectAttempts = Number.POSITIVE_INFINITY;
|
|
38
|
+
private readonly baseReconnectDelay = 1000;
|
|
39
|
+
private readonly maxReconnectDelay = 30_000;
|
|
40
|
+
|
|
41
|
+
constructor(options: FlagixClientOptions) {
|
|
42
|
+
this.apiKey = options.apiKey;
|
|
43
|
+
this.apiBaseUrl = options.apiBaseUrl.replace(REMOVE_TRAILING_SLASH, "");
|
|
44
|
+
this.context = options.initialContext || {};
|
|
45
|
+
this.emitter = new FlagixEventEmitter();
|
|
46
|
+
setLogLevel(options.logs?.level ?? "none");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Subscribes a listener to a flag update event.
|
|
51
|
+
*/
|
|
52
|
+
on(
|
|
53
|
+
event: typeof FLAG_UPDATE_EVENT,
|
|
54
|
+
listener: (flagKey: string) => void
|
|
55
|
+
): void {
|
|
56
|
+
this.emitter.on(event, listener);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Unsubscribes a listener from a flag update event.
|
|
61
|
+
*/
|
|
62
|
+
off(
|
|
63
|
+
event: typeof FLAG_UPDATE_EVENT,
|
|
64
|
+
listener: (flagKey: string) => void
|
|
65
|
+
): void {
|
|
66
|
+
this.emitter.off(event, listener);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetches all flag configurations from the API, populates the local cache,
|
|
71
|
+
* and sets up the SSE connection for real-time updates.
|
|
72
|
+
*/
|
|
73
|
+
async initialize(): Promise<void> {
|
|
74
|
+
if (this.isInitialized) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
log("info", "[Flagix SDK] Starting initialization...");
|
|
79
|
+
|
|
80
|
+
await this.fetchInitialConfig();
|
|
81
|
+
this.setupSSEListener();
|
|
82
|
+
|
|
83
|
+
this.isInitialized = true;
|
|
84
|
+
log("info", "[Flagix SDK] Initialization complete.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true if the client has completed initialization
|
|
89
|
+
*/
|
|
90
|
+
getIsInitialized(): boolean {
|
|
91
|
+
return this.isInitialized;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Evaluates a flag locally using the cached configuration and current context.
|
|
96
|
+
* @param flagKey The key of the flag to evaluate.
|
|
97
|
+
* @param context Optional, temporary context overrides for this specific evaluation.
|
|
98
|
+
*/
|
|
99
|
+
evaluate<T extends VariationValue>(
|
|
100
|
+
flagKey: string,
|
|
101
|
+
contextOverrides?: EvaluationContext
|
|
102
|
+
): T | null {
|
|
103
|
+
if (!this.isInitialized) {
|
|
104
|
+
log(
|
|
105
|
+
"warn",
|
|
106
|
+
`[Flagix SDK] Not initialized. Cannot evaluate flag: ${flagKey}`
|
|
107
|
+
);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const config = this.localCache.get(flagKey);
|
|
112
|
+
|
|
113
|
+
if (!config) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const finalContext = { ...this.context, ...contextOverrides };
|
|
118
|
+
|
|
119
|
+
const result = evaluateFlag(config, finalContext);
|
|
120
|
+
|
|
121
|
+
if (result) {
|
|
122
|
+
this.trackEvaluation(flagKey, result, finalContext);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (result?.value as T) ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sets or updates the global evaluation context.
|
|
130
|
+
* @param newContext New context attributes to merge or replace.
|
|
131
|
+
*/
|
|
132
|
+
setContext(newContext: EvaluationContext): void {
|
|
133
|
+
this.context = { ...this.context, ...newContext };
|
|
134
|
+
log(
|
|
135
|
+
"info",
|
|
136
|
+
"[Flagix SDK] Context updated. Evaluations will use the new context."
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async fetchInitialConfig(): Promise<void> {
|
|
141
|
+
const url = `${this.apiBaseUrl}/api/flag-config/all`;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(url, {
|
|
145
|
+
headers: { "X-Api-Key": this.apiKey },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Failed to load initial config: ${response.statusText}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const allFlags = (await response.json()) as Record<string, FlagConfig>;
|
|
155
|
+
|
|
156
|
+
this.localCache.clear();
|
|
157
|
+
for (const [key, config] of Object.entries(allFlags)) {
|
|
158
|
+
this.localCache.set(key, config);
|
|
159
|
+
}
|
|
160
|
+
log("info", `[Flagix SDK] Loaded ${this.localCache.size} flag configs.`);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
log(
|
|
163
|
+
"error",
|
|
164
|
+
"[Flagix SDK] CRITICAL: Initial configuration fetch failed.",
|
|
165
|
+
error
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async setupSSEListener(): Promise<void> {
|
|
171
|
+
if (this.sseConnection) {
|
|
172
|
+
try {
|
|
173
|
+
this.sseConnection.close();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
log(
|
|
176
|
+
"warn",
|
|
177
|
+
"[Flagix SDK] Error closing existing SSE connection",
|
|
178
|
+
error
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
this.sseConnection = null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const url = `${this.apiBaseUrl}/api/sse/stream`;
|
|
185
|
+
|
|
186
|
+
const source = await createEventSource(url, this.apiKey);
|
|
187
|
+
if (!source) {
|
|
188
|
+
log("warn", "[Flagix SDK] Failed to create EventSource. Retrying...");
|
|
189
|
+
this.scheduleReconnect();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.sseConnection = source;
|
|
194
|
+
|
|
195
|
+
source.onopen = () => {
|
|
196
|
+
this.reconnectAttempts = 0;
|
|
197
|
+
this.isReconnecting = false;
|
|
198
|
+
if (this.reconnectTimeoutId) {
|
|
199
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
200
|
+
this.reconnectTimeoutId = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// If this is a reconnection and not the first connection, refresh the cache
|
|
204
|
+
// this ensures we have the latest flag values that may have changed while disconnected
|
|
205
|
+
if (this.hasEstablishedConnection && this.isInitialized) {
|
|
206
|
+
log(
|
|
207
|
+
"info",
|
|
208
|
+
"[Flagix SDK] SSE reconnected. Refreshing cache to sync with server..."
|
|
209
|
+
);
|
|
210
|
+
this.fetchInitialConfig().catch((error) => {
|
|
211
|
+
log(
|
|
212
|
+
"error",
|
|
213
|
+
"[Flagix SDK] Failed to refresh cache after reconnection",
|
|
214
|
+
error
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
this.hasEstablishedConnection = true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
log("info", "[Flagix SDK] SSE connection established.");
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
source.onerror = (error) => {
|
|
225
|
+
const eventSource = error.target as EventSource;
|
|
226
|
+
const readyState = eventSource?.readyState;
|
|
227
|
+
|
|
228
|
+
// EventSource.readyState: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED
|
|
229
|
+
if (readyState === 2) {
|
|
230
|
+
log(
|
|
231
|
+
"warn",
|
|
232
|
+
"[Flagix SDK] SSE connection closed. Attempting to reconnect..."
|
|
233
|
+
);
|
|
234
|
+
this.handleReconnect();
|
|
235
|
+
} else if (readyState === 0) {
|
|
236
|
+
log(
|
|
237
|
+
"warn",
|
|
238
|
+
"[Flagix SDK] SSE connection error (connecting state)",
|
|
239
|
+
error
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
log("error", "[Flagix SDK] SSE error", error);
|
|
243
|
+
this.handleReconnect();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Listen for the "connected" event from the server
|
|
248
|
+
source.addEventListener("connected", () => {
|
|
249
|
+
log("info", "[Flagix SDK] SSE connection confirmed by server.");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
source.addEventListener(EVENT_TO_LISTEN, (event) => {
|
|
253
|
+
try {
|
|
254
|
+
const data = JSON.parse(event.data);
|
|
255
|
+
const { flagKey, type } = data as {
|
|
256
|
+
flagKey: string;
|
|
257
|
+
type: FlagUpdateType;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
log("info", `[Flagix SDK] Received update for ${flagKey} (${type}).`);
|
|
261
|
+
|
|
262
|
+
this.fetchSingleFlagConfig(flagKey, type);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
log("error", "[Flagix SDK] Failed to parse SSE event data.", error);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private handleReconnect(): void {
|
|
270
|
+
if (this.isReconnecting || !this.isInitialized) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
275
|
+
log(
|
|
276
|
+
"error",
|
|
277
|
+
"[Flagix SDK] Max reconnection attempts reached. Stopping reconnection."
|
|
278
|
+
);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.isReconnecting = true;
|
|
283
|
+
this.scheduleReconnect();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private scheduleReconnect(): void {
|
|
287
|
+
if (this.reconnectTimeoutId) {
|
|
288
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Calculate exponential backoff delay with jitter
|
|
292
|
+
const delay = Math.min(
|
|
293
|
+
this.baseReconnectDelay * 2 ** this.reconnectAttempts,
|
|
294
|
+
this.maxReconnectDelay
|
|
295
|
+
);
|
|
296
|
+
// Add ±25% jitter to prevent thundering herd
|
|
297
|
+
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
298
|
+
const finalDelay = Math.max(100, delay + jitter);
|
|
299
|
+
|
|
300
|
+
this.reconnectAttempts++;
|
|
301
|
+
|
|
302
|
+
log(
|
|
303
|
+
"info",
|
|
304
|
+
`[Flagix SDK] Scheduling SSE reconnection attempt ${this.reconnectAttempts} in ${Math.round(finalDelay)}ms...`
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
this.reconnectTimeoutId = setTimeout(() => {
|
|
308
|
+
this.isReconnecting = false;
|
|
309
|
+
this.reconnectTimeoutId = null;
|
|
310
|
+
this.setupSSEListener().catch((error) => {
|
|
311
|
+
log("error", "[Flagix SDK] Failed to reconnect SSE", error);
|
|
312
|
+
this.handleReconnect();
|
|
313
|
+
});
|
|
314
|
+
}, finalDelay);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async fetchSingleFlagConfig(
|
|
318
|
+
flagKey: string,
|
|
319
|
+
type: FlagUpdateType
|
|
320
|
+
): Promise<void> {
|
|
321
|
+
const url = `${this.apiBaseUrl}/api/flag-config/${flagKey}`;
|
|
322
|
+
|
|
323
|
+
if (type === "FLAG_DELETED" || type === "RULE_DELETED") {
|
|
324
|
+
this.localCache.delete(flagKey);
|
|
325
|
+
log("info", `[Flagix SDK] Flag ${flagKey} deleted from cache.`);
|
|
326
|
+
this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const response = await fetch(url, {
|
|
332
|
+
headers: { "X-Api-Key": this.apiKey },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (response.status === 404) {
|
|
336
|
+
this.localCache.delete(flagKey);
|
|
337
|
+
|
|
338
|
+
log(
|
|
339
|
+
"warn",
|
|
340
|
+
`[Flagix SDK] Flag ${flagKey} not found on API, deleted from cache.`
|
|
341
|
+
);
|
|
342
|
+
this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Failed to fetch update for ${flagKey}: ${response.statusText}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const config = (await response.json()) as FlagConfig;
|
|
353
|
+
|
|
354
|
+
this.localCache.set(flagKey, config);
|
|
355
|
+
log("info", `[Flagix SDK] Flag ${flagKey} updated/synced.`);
|
|
356
|
+
this.emitter.emit(FLAG_UPDATE_EVENT, flagKey);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
log(
|
|
359
|
+
"error",
|
|
360
|
+
`[Flagix SDK] Failed to fetch update for ${flagKey}.`,
|
|
361
|
+
error
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Records a custom event (conversion) for analytics and A/B testing.
|
|
368
|
+
*/
|
|
369
|
+
track(
|
|
370
|
+
eventName: string,
|
|
371
|
+
properties?: Record<string, unknown>,
|
|
372
|
+
contextOverrides?: EvaluationContext
|
|
373
|
+
): void {
|
|
374
|
+
const url = `${this.apiBaseUrl}/api/track/event`;
|
|
375
|
+
|
|
376
|
+
const finalContext = { ...this.context, ...contextOverrides };
|
|
377
|
+
const distinctId = resolveIdentifier(finalContext);
|
|
378
|
+
|
|
379
|
+
const payload = {
|
|
380
|
+
apiKey: this.apiKey,
|
|
381
|
+
event_name: eventName,
|
|
382
|
+
distinctId,
|
|
383
|
+
properties: properties || {},
|
|
384
|
+
timestamp: new Date().toISOString(),
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const payloadJson = JSON.stringify(payload);
|
|
388
|
+
|
|
389
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
390
|
+
const blob = new Blob([payloadJson], { type: "application/json" });
|
|
391
|
+
if (navigator.sendBeacon(url, blob)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.fireAndForgetFetch(url, payloadJson);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Closes the Server-Sent Events (SSE) connection and cleans up resources.
|
|
401
|
+
*/
|
|
402
|
+
close(): void {
|
|
403
|
+
if (this.reconnectTimeoutId) {
|
|
404
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
405
|
+
this.reconnectTimeoutId = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.isReconnecting = false;
|
|
409
|
+
this.reconnectAttempts = 0;
|
|
410
|
+
this.hasEstablishedConnection = false;
|
|
411
|
+
|
|
412
|
+
if (this.sseConnection) {
|
|
413
|
+
try {
|
|
414
|
+
this.sseConnection.close();
|
|
415
|
+
} catch (error) {
|
|
416
|
+
log("warn", "[Flagix SDK] Error closing SSE connection", error);
|
|
417
|
+
}
|
|
418
|
+
this.sseConnection = null;
|
|
419
|
+
log("info", "[Flagix SDK] SSE connection closed.");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.localCache.clear();
|
|
423
|
+
this.isInitialized = false;
|
|
424
|
+
this.emitter.removeAllListeners();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Asynchronously sends an evaluation event to the backend tracking service.
|
|
429
|
+
*/
|
|
430
|
+
private trackEvaluation(
|
|
431
|
+
flagKey: string,
|
|
432
|
+
result: FlagVariation,
|
|
433
|
+
context: EvaluationContext
|
|
434
|
+
): void {
|
|
435
|
+
const url = `${this.apiBaseUrl}/api/track/evaluation`;
|
|
436
|
+
|
|
437
|
+
const distinctId = resolveIdentifier(context);
|
|
438
|
+
|
|
439
|
+
const payload = {
|
|
440
|
+
apiKey: this.apiKey,
|
|
441
|
+
flagKey,
|
|
442
|
+
variationName: result.name,
|
|
443
|
+
variationValue: result.value,
|
|
444
|
+
variationType: result.type,
|
|
445
|
+
distinctId,
|
|
446
|
+
evaluationContext: context,
|
|
447
|
+
evaluatedAt: new Date().toISOString(),
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const payloadJson = JSON.stringify(payload);
|
|
451
|
+
|
|
452
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
453
|
+
const blob = new Blob([payloadJson], { type: "application/json" });
|
|
454
|
+
|
|
455
|
+
const success = navigator.sendBeacon(url, blob);
|
|
456
|
+
|
|
457
|
+
if (success) {
|
|
458
|
+
log("info", `Successfully queued beacon for ${flagKey}.`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
log("warn", `Beacon queue full for ${flagKey}. Falling back to fetch.`);
|
|
463
|
+
this.fireAndForgetFetch(url, payloadJson);
|
|
464
|
+
} else {
|
|
465
|
+
this.fireAndForgetFetch(url, payloadJson);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private fireAndForgetFetch(url: string, payloadJson: string): void {
|
|
470
|
+
fetch(url, {
|
|
471
|
+
method: "POST",
|
|
472
|
+
headers: { "Content-Type": "application/json" },
|
|
473
|
+
body: payloadJson,
|
|
474
|
+
keepalive: true,
|
|
475
|
+
}).catch((error) => {
|
|
476
|
+
log(
|
|
477
|
+
"error",
|
|
478
|
+
`Critical failure sending impression event via fetch: ${error.message}`
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { FlagixClient } from "@/client";
|
|
2
|
+
import { FLAG_UPDATE_EVENT } from "@/lib/emitter";
|
|
3
|
+
import { log } from "@/lib/logger";
|
|
4
|
+
import type {
|
|
5
|
+
EvaluationContext,
|
|
6
|
+
FlagixClientOptions,
|
|
7
|
+
VariationValue,
|
|
8
|
+
} from "@/types";
|
|
9
|
+
|
|
10
|
+
let clientInstance: FlagixClient | null = null;
|
|
11
|
+
|
|
12
|
+
let isInitializing = false;
|
|
13
|
+
let initializationPromise: Promise<void> | null = null;
|
|
14
|
+
|
|
15
|
+
export const Flagix = {
|
|
16
|
+
/**
|
|
17
|
+
* Initializes the Flagix SDK, fetches all flags, and sets up an SSE connection.
|
|
18
|
+
*/
|
|
19
|
+
async initialize(options: FlagixClientOptions): Promise<void> {
|
|
20
|
+
if (clientInstance) {
|
|
21
|
+
log("warn", "Flagix SDK already initialized. Ignoring subsequent call.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isInitializing && initializationPromise) {
|
|
26
|
+
return initializationPromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isInitializing = true;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
clientInstance = new FlagixClient(options);
|
|
33
|
+
initializationPromise = clientInstance.initialize();
|
|
34
|
+
await initializationPromise;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
log("error", "Flagix SDK failed during initialization:", error);
|
|
37
|
+
throw error;
|
|
38
|
+
} finally {
|
|
39
|
+
isInitializing = false;
|
|
40
|
+
initializationPromise = null;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Evaluates a flag based on the local cache and current context.
|
|
46
|
+
* @param flagKey The key of the flag to evaluate.
|
|
47
|
+
* @param contextOverrides Optional, temporary context overrides.
|
|
48
|
+
*/
|
|
49
|
+
evaluate<T extends VariationValue>(
|
|
50
|
+
flagKey: string,
|
|
51
|
+
contextOverrides?: EvaluationContext
|
|
52
|
+
): T | null {
|
|
53
|
+
if (!clientInstance || !clientInstance.getIsInitialized()) {
|
|
54
|
+
log(
|
|
55
|
+
"error",
|
|
56
|
+
"Flagix SDK not initialized. Call Flagix.initialize() first."
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return clientInstance.evaluate<T>(flagKey, contextOverrides);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Records a custom event for analytics.
|
|
66
|
+
* @param eventName The name of the event.
|
|
67
|
+
* @param properties Optional custom metadata.
|
|
68
|
+
* @param contextOverrides Optional context.
|
|
69
|
+
*/
|
|
70
|
+
track(
|
|
71
|
+
eventName: string,
|
|
72
|
+
properties?: Record<string, unknown>,
|
|
73
|
+
contextOverrides?: EvaluationContext
|
|
74
|
+
): void {
|
|
75
|
+
if (!clientInstance) {
|
|
76
|
+
log(
|
|
77
|
+
"error",
|
|
78
|
+
"Flagix SDK not initialized. Call Flagix.initialize() first."
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
clientInstance.track(eventName, properties, contextOverrides);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sets or updates the global evaluation context.
|
|
87
|
+
* @param newContext New context attributes to merge or replace.
|
|
88
|
+
*/
|
|
89
|
+
setContext(newContext: EvaluationContext): void {
|
|
90
|
+
if (!clientInstance) {
|
|
91
|
+
log("error", "Flagix SDK not initialized.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
clientInstance.setContext(newContext);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Closes the SSE connection and cleans up resources.
|
|
99
|
+
*/
|
|
100
|
+
close(): void {
|
|
101
|
+
if (clientInstance) {
|
|
102
|
+
clientInstance.close();
|
|
103
|
+
clientInstance = null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* checks initialization status
|
|
109
|
+
*/
|
|
110
|
+
isInitialized(): boolean {
|
|
111
|
+
return !!clientInstance && clientInstance.getIsInitialized();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Subscribes a listener to updates for any flag.
|
|
116
|
+
* @param listener The callback function (receives the updated flagKey).
|
|
117
|
+
*/
|
|
118
|
+
onFlagUpdate(listener: (flagKey: string) => void): void {
|
|
119
|
+
if (!clientInstance) {
|
|
120
|
+
log("warn", "Flagix SDK not initialized. Cannot subscribe to updates.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
clientInstance.on(FLAG_UPDATE_EVENT, listener);
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Unsubscribes a listener from flag updates.
|
|
128
|
+
* @param listener The callback function to remove.
|
|
129
|
+
*/
|
|
130
|
+
offFlagUpdate(listener: (flagKey: string) => void): void {
|
|
131
|
+
if (!clientInstance) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
clientInstance.off(FLAG_UPDATE_EVENT, listener);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// biome-ignore lint/performance/noBarrelFile: <>
|
|
139
|
+
export * from "./types";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const REMOVE_TRAILING_SLASH = /\/$/;
|
|
2
|
+
|
|
3
|
+
export type FlagUpdateType =
|
|
4
|
+
| "FLAG_CREATED"
|
|
5
|
+
| "FLAG_UPDATED"
|
|
6
|
+
| "FLAG_DELETED"
|
|
7
|
+
| "FLAG_METADATA_UPDATED"
|
|
8
|
+
| "FLAG_STATE_TOGGLED"
|
|
9
|
+
| "RULE_UPDATED"
|
|
10
|
+
| "RULE_DELETED";
|
|
11
|
+
|
|
12
|
+
export const EVENT_TO_LISTEN = "flag-update";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import EventEmitter from "eventemitter3";
|
|
2
|
+
import { EVENT_TO_LISTEN } from "@/lib/constants";
|
|
3
|
+
|
|
4
|
+
export type FlagixEvents = {
|
|
5
|
+
"flag-update": (flagKey: string) => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const FLAG_UPDATE_EVENT = EVENT_TO_LISTEN;
|
|
9
|
+
|
|
10
|
+
export class FlagixEventEmitter extends EventEmitter<FlagixEvents> {}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 'info' is for detailed debugging information
|
|
3
|
+
* 'warn' is for non-critical issues
|
|
4
|
+
* 'error' is for critical failures
|
|
5
|
+
* 'none' suppresses all output.
|
|
6
|
+
*/
|
|
7
|
+
export type LogLevel = "none" | "error" | "warn" | "info";
|
|
8
|
+
|
|
9
|
+
let currentLogLevel: LogLevel = "none";
|
|
10
|
+
|
|
11
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
12
|
+
none: 0,
|
|
13
|
+
error: 1,
|
|
14
|
+
warn: 2,
|
|
15
|
+
info: 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function setLogLevel(level: LogLevel = "none"): void {
|
|
19
|
+
currentLogLevel = level;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function log(
|
|
23
|
+
level: "error" | "warn" | "info",
|
|
24
|
+
message: string,
|
|
25
|
+
...args: unknown[]
|
|
26
|
+
): void {
|
|
27
|
+
const currentLevel = LOG_LEVELS[currentLogLevel];
|
|
28
|
+
const requiredLevel = LOG_LEVELS[level];
|
|
29
|
+
|
|
30
|
+
if (currentLevel >= requiredLevel) {
|
|
31
|
+
const prefixedMessage = `[Flagix SDK] ${message}`;
|
|
32
|
+
|
|
33
|
+
if (level === "error") {
|
|
34
|
+
console.error(prefixedMessage, ...args);
|
|
35
|
+
} else if (level === "warn") {
|
|
36
|
+
console.warn(prefixedMessage, ...args);
|
|
37
|
+
} else {
|
|
38
|
+
console.info(prefixedMessage, ...args);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|