@flipswitch-io/sdk 0.1.5 → 0.1.7
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 +301 -55
- package/dist/index.d.mts +194 -8
- package/dist/index.d.ts +194 -8
- package/dist/index.js +435 -23
- package/dist/index.mjs +434 -23
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -9,26 +9,37 @@ import { OFREPWebProvider } from "@openfeature/ofrep-web-provider";
|
|
|
9
9
|
var MIN_RETRY_DELAY = 1e3;
|
|
10
10
|
var MAX_RETRY_DELAY = 3e4;
|
|
11
11
|
var SseClient = class {
|
|
12
|
-
constructor(baseUrl, apiKey, onFlagChange, onStatusChange, telemetryHeaders) {
|
|
12
|
+
constructor(baseUrl, apiKey, onFlagChange, onStatusChange, telemetryHeaders, enableVisibilityHandling = true) {
|
|
13
13
|
this.baseUrl = baseUrl;
|
|
14
14
|
this.apiKey = apiKey;
|
|
15
15
|
this.onFlagChange = onFlagChange;
|
|
16
16
|
this.onStatusChange = onStatusChange;
|
|
17
17
|
this.telemetryHeaders = telemetryHeaders;
|
|
18
|
+
this.enableVisibilityHandling = enableVisibilityHandling;
|
|
18
19
|
this.eventSource = null;
|
|
19
20
|
this.retryDelay = MIN_RETRY_DELAY;
|
|
20
21
|
this.reconnectTimeout = null;
|
|
21
22
|
this.closed = false;
|
|
23
|
+
this.paused = false;
|
|
22
24
|
this.status = "disconnected";
|
|
25
|
+
this.visibilityHandler = null;
|
|
26
|
+
this.abortController = null;
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* Start the SSE connection.
|
|
26
30
|
*/
|
|
27
31
|
connect() {
|
|
28
|
-
if (this.closed) return;
|
|
32
|
+
if (this.closed || this.paused) return;
|
|
33
|
+
if (this.abortController) {
|
|
34
|
+
this.abortController.abort();
|
|
35
|
+
}
|
|
36
|
+
this.abortController = new AbortController();
|
|
29
37
|
if (this.eventSource) {
|
|
30
38
|
this.eventSource.close();
|
|
31
39
|
}
|
|
40
|
+
if (this.enableVisibilityHandling) {
|
|
41
|
+
this.setupVisibilityHandling();
|
|
42
|
+
}
|
|
32
43
|
this.updateStatus("connecting");
|
|
33
44
|
const url = `${this.baseUrl}/api/v1/flags/events`;
|
|
34
45
|
try {
|
|
@@ -38,11 +49,62 @@ var SseClient = class {
|
|
|
38
49
|
this.connectWithPolyfill(url);
|
|
39
50
|
}
|
|
40
51
|
} catch (error) {
|
|
41
|
-
console.
|
|
52
|
+
console.warn("[Flipswitch] Failed to establish SSE connection:", error);
|
|
42
53
|
this.updateStatus("error");
|
|
43
54
|
this.scheduleReconnect();
|
|
44
55
|
}
|
|
45
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Setup visibility API handling.
|
|
59
|
+
* Disconnects when tab is hidden, reconnects when visible.
|
|
60
|
+
*/
|
|
61
|
+
setupVisibilityHandling() {
|
|
62
|
+
if (typeof document === "undefined" || this.visibilityHandler) return;
|
|
63
|
+
this.visibilityHandler = () => {
|
|
64
|
+
if (document.hidden) {
|
|
65
|
+
this.pause();
|
|
66
|
+
} else {
|
|
67
|
+
this.resume();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Pause the SSE connection (used by visibility API).
|
|
74
|
+
* Different from close() - can be resumed.
|
|
75
|
+
*/
|
|
76
|
+
pause() {
|
|
77
|
+
if (this.paused || this.closed) return;
|
|
78
|
+
this.paused = true;
|
|
79
|
+
if (this.abortController) {
|
|
80
|
+
this.abortController.abort();
|
|
81
|
+
this.abortController = null;
|
|
82
|
+
}
|
|
83
|
+
if (this.reconnectTimeout) {
|
|
84
|
+
clearTimeout(this.reconnectTimeout);
|
|
85
|
+
this.reconnectTimeout = null;
|
|
86
|
+
}
|
|
87
|
+
if (this.eventSource) {
|
|
88
|
+
this.eventSource.close();
|
|
89
|
+
this.eventSource = null;
|
|
90
|
+
}
|
|
91
|
+
this.updateStatus("disconnected");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resume the SSE connection after being paused.
|
|
95
|
+
*/
|
|
96
|
+
resume() {
|
|
97
|
+
if (!this.paused || this.closed) return;
|
|
98
|
+
this.paused = false;
|
|
99
|
+
this.retryDelay = MIN_RETRY_DELAY;
|
|
100
|
+
this.connect();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if the connection is paused.
|
|
104
|
+
*/
|
|
105
|
+
isPaused() {
|
|
106
|
+
return this.paused;
|
|
107
|
+
}
|
|
46
108
|
/**
|
|
47
109
|
* Connect using fetch-based SSE (supports custom headers).
|
|
48
110
|
*/
|
|
@@ -56,7 +118,8 @@ var SseClient = class {
|
|
|
56
118
|
};
|
|
57
119
|
const response = await fetch(url, {
|
|
58
120
|
method: "GET",
|
|
59
|
-
headers
|
|
121
|
+
headers,
|
|
122
|
+
signal: this.abortController?.signal
|
|
60
123
|
});
|
|
61
124
|
if (!response.ok) {
|
|
62
125
|
throw new Error(`SSE connection failed: ${response.status}`);
|
|
@@ -97,14 +160,14 @@ var SseClient = class {
|
|
|
97
160
|
};
|
|
98
161
|
processStream().catch((error) => {
|
|
99
162
|
if (!this.closed) {
|
|
100
|
-
console.
|
|
163
|
+
console.warn("[Flipswitch] SSE stream error:", error);
|
|
101
164
|
this.updateStatus("error");
|
|
102
165
|
this.scheduleReconnect();
|
|
103
166
|
}
|
|
104
167
|
});
|
|
105
168
|
} catch (error) {
|
|
106
169
|
if (!this.closed) {
|
|
107
|
-
console.
|
|
170
|
+
console.warn("[Flipswitch] SSE connection error:", error);
|
|
108
171
|
this.updateStatus("error");
|
|
109
172
|
this.scheduleReconnect();
|
|
110
173
|
}
|
|
@@ -134,20 +197,21 @@ var SseClient = class {
|
|
|
134
197
|
this.onFlagChange(event);
|
|
135
198
|
} else if (eventType === "config-updated") {
|
|
136
199
|
const parsed = JSON.parse(data);
|
|
137
|
-
if (parsed.reason === "api-key-rotated") {
|
|
138
|
-
console.warn(
|
|
139
|
-
"[Flipswitch] API key has been rotated. You may need to update your API key configuration."
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
200
|
const event = {
|
|
143
201
|
flagKey: null,
|
|
144
202
|
// null indicates all flags should be refreshed
|
|
145
203
|
timestamp: parsed.timestamp
|
|
146
204
|
};
|
|
147
205
|
this.onFlagChange(event);
|
|
148
|
-
} else if (eventType === "
|
|
149
|
-
const
|
|
150
|
-
|
|
206
|
+
} else if (eventType === "api-key-rotated") {
|
|
207
|
+
const parsed = JSON.parse(data);
|
|
208
|
+
if (!parsed.validUntil) {
|
|
209
|
+
console.info("[Flipswitch] API key rotation was aborted");
|
|
210
|
+
} else {
|
|
211
|
+
console.warn(
|
|
212
|
+
`[Flipswitch] API key was rotated. Current key valid until: ${parsed.validUntil}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
151
215
|
}
|
|
152
216
|
} catch (error) {
|
|
153
217
|
console.error(`[Flipswitch] Failed to parse ${eventType} event:`, error);
|
|
@@ -187,6 +251,14 @@ var SseClient = class {
|
|
|
187
251
|
close() {
|
|
188
252
|
this.closed = true;
|
|
189
253
|
this.updateStatus("disconnected");
|
|
254
|
+
if (this.visibilityHandler && typeof document !== "undefined") {
|
|
255
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
256
|
+
this.visibilityHandler = null;
|
|
257
|
+
}
|
|
258
|
+
if (this.abortController) {
|
|
259
|
+
this.abortController.abort();
|
|
260
|
+
this.abortController = null;
|
|
261
|
+
}
|
|
190
262
|
if (this.reconnectTimeout) {
|
|
191
263
|
clearTimeout(this.reconnectTimeout);
|
|
192
264
|
this.reconnectTimeout = null;
|
|
@@ -198,9 +270,204 @@ var SseClient = class {
|
|
|
198
270
|
}
|
|
199
271
|
};
|
|
200
272
|
|
|
273
|
+
// src/browser-cache.ts
|
|
274
|
+
var CACHE_PREFIX = "flipswitch:flags:";
|
|
275
|
+
var CACHE_META_KEY = "flipswitch:cache-meta";
|
|
276
|
+
var BrowserCache = class {
|
|
277
|
+
constructor() {
|
|
278
|
+
this.cacheVersion = 1;
|
|
279
|
+
this.storage = this.getStorage();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Get storage instance if available.
|
|
283
|
+
*/
|
|
284
|
+
getStorage() {
|
|
285
|
+
if (typeof window === "undefined") return null;
|
|
286
|
+
try {
|
|
287
|
+
const testKey = "__flipswitch_test__";
|
|
288
|
+
window.localStorage.setItem(testKey, "test");
|
|
289
|
+
window.localStorage.removeItem(testKey);
|
|
290
|
+
return window.localStorage;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Check if browser cache is available.
|
|
297
|
+
*/
|
|
298
|
+
isAvailable() {
|
|
299
|
+
return this.storage !== null;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get a cached flag value.
|
|
303
|
+
*/
|
|
304
|
+
get(flagKey) {
|
|
305
|
+
if (!this.storage) return null;
|
|
306
|
+
try {
|
|
307
|
+
const key = CACHE_PREFIX + flagKey;
|
|
308
|
+
const item = this.storage.getItem(key);
|
|
309
|
+
if (!item) return null;
|
|
310
|
+
const cached = JSON.parse(item);
|
|
311
|
+
return cached;
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Set a cached flag value.
|
|
318
|
+
*/
|
|
319
|
+
set(flagKey, value, variant, reason) {
|
|
320
|
+
if (!this.storage) return;
|
|
321
|
+
try {
|
|
322
|
+
const key = CACHE_PREFIX + flagKey;
|
|
323
|
+
const cached = {
|
|
324
|
+
value,
|
|
325
|
+
timestamp: Date.now(),
|
|
326
|
+
variant,
|
|
327
|
+
reason
|
|
328
|
+
};
|
|
329
|
+
this.storage.setItem(key, JSON.stringify(cached));
|
|
330
|
+
this.updateMeta();
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Set multiple cached flag values at once.
|
|
336
|
+
*/
|
|
337
|
+
setAll(flags) {
|
|
338
|
+
if (!this.storage) return;
|
|
339
|
+
try {
|
|
340
|
+
for (const flag of flags) {
|
|
341
|
+
const cacheKey = CACHE_PREFIX + flag.key;
|
|
342
|
+
const cached = {
|
|
343
|
+
value: flag.value,
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
variant: flag.variant,
|
|
346
|
+
reason: flag.reason
|
|
347
|
+
};
|
|
348
|
+
this.storage.setItem(cacheKey, JSON.stringify(cached));
|
|
349
|
+
}
|
|
350
|
+
this.updateMeta();
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Get all cached flags.
|
|
356
|
+
*/
|
|
357
|
+
getAll() {
|
|
358
|
+
const result = /* @__PURE__ */ new Map();
|
|
359
|
+
if (!this.storage) return result;
|
|
360
|
+
try {
|
|
361
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
362
|
+
const key = this.storage.key(i);
|
|
363
|
+
if (key?.startsWith(CACHE_PREFIX)) {
|
|
364
|
+
const flagKey = key.slice(CACHE_PREFIX.length);
|
|
365
|
+
const item = this.storage.getItem(key);
|
|
366
|
+
if (item) {
|
|
367
|
+
const cached = JSON.parse(item);
|
|
368
|
+
result.set(flagKey, cached);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Invalidate a specific flag or all flags.
|
|
378
|
+
*/
|
|
379
|
+
invalidate(flagKey) {
|
|
380
|
+
if (!this.storage) return;
|
|
381
|
+
try {
|
|
382
|
+
if (flagKey) {
|
|
383
|
+
this.storage.removeItem(CACHE_PREFIX + flagKey);
|
|
384
|
+
} else {
|
|
385
|
+
const keysToRemove = [];
|
|
386
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
387
|
+
const key = this.storage.key(i);
|
|
388
|
+
if (key?.startsWith(CACHE_PREFIX)) {
|
|
389
|
+
keysToRemove.push(key);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const key of keysToRemove) {
|
|
393
|
+
this.storage.removeItem(key);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
this.updateMeta();
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Update cache metadata.
|
|
402
|
+
*/
|
|
403
|
+
updateMeta() {
|
|
404
|
+
if (!this.storage) return;
|
|
405
|
+
try {
|
|
406
|
+
const meta = {
|
|
407
|
+
lastUpdated: Date.now(),
|
|
408
|
+
version: this.cacheVersion
|
|
409
|
+
};
|
|
410
|
+
this.storage.setItem(CACHE_META_KEY, JSON.stringify(meta));
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get cache metadata.
|
|
416
|
+
*/
|
|
417
|
+
getMeta() {
|
|
418
|
+
if (!this.storage) return null;
|
|
419
|
+
try {
|
|
420
|
+
const item = this.storage.getItem(CACHE_META_KEY);
|
|
421
|
+
if (!item) return null;
|
|
422
|
+
return JSON.parse(item);
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Get the age of a cached flag in milliseconds.
|
|
429
|
+
*/
|
|
430
|
+
getAge(flagKey) {
|
|
431
|
+
const cached = this.get(flagKey);
|
|
432
|
+
if (!cached) return null;
|
|
433
|
+
return Date.now() - cached.timestamp;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Check if a cached flag is stale based on max age.
|
|
437
|
+
*/
|
|
438
|
+
isStale(flagKey, maxAgeMs) {
|
|
439
|
+
const age = this.getAge(flagKey);
|
|
440
|
+
if (age === null) return true;
|
|
441
|
+
return age > maxAgeMs;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Clear all Flipswitch data from localStorage.
|
|
445
|
+
*/
|
|
446
|
+
clear() {
|
|
447
|
+
if (!this.storage) return;
|
|
448
|
+
try {
|
|
449
|
+
const keysToRemove = [];
|
|
450
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
451
|
+
const key = this.storage.key(i);
|
|
452
|
+
if (key?.startsWith("flipswitch:")) {
|
|
453
|
+
keysToRemove.push(key);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const key of keysToRemove) {
|
|
457
|
+
this.storage.removeItem(key);
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// package.json
|
|
465
|
+
var version = "0.1.7";
|
|
466
|
+
|
|
201
467
|
// src/provider.ts
|
|
202
468
|
var DEFAULT_BASE_URL = "https://api.flipswitch.io";
|
|
203
|
-
var
|
|
469
|
+
var DEFAULT_POLLING_INTERVAL = 3e4;
|
|
470
|
+
var DEFAULT_MAX_SSE_RETRIES = 5;
|
|
204
471
|
var FlipswitchProvider = class {
|
|
205
472
|
constructor(options, eventHandlers) {
|
|
206
473
|
this.metadata = {
|
|
@@ -211,11 +478,28 @@ var FlipswitchProvider = class {
|
|
|
211
478
|
this._status = ClientProviderStatus.NOT_READY;
|
|
212
479
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
213
480
|
this.userEventHandlers = {};
|
|
481
|
+
this.pollingTimer = null;
|
|
482
|
+
this.sseRetryCount = 0;
|
|
483
|
+
this.isPollingFallbackActive = false;
|
|
484
|
+
this.onlineHandler = null;
|
|
485
|
+
this.offlineHandler = null;
|
|
486
|
+
this._isOnline = true;
|
|
214
487
|
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
215
488
|
this.apiKey = options.apiKey;
|
|
216
489
|
this.enableRealtime = options.enableRealtime ?? true;
|
|
217
490
|
this.fetchImpl = options.fetchImplementation ?? (typeof window !== "undefined" ? fetch.bind(window) : fetch);
|
|
218
491
|
this.userEventHandlers = eventHandlers ?? {};
|
|
492
|
+
const isBrowser = typeof window !== "undefined";
|
|
493
|
+
this.enableVisibilityHandling = options.enableVisibilityHandling ?? isBrowser;
|
|
494
|
+
this.offlineMode = options.offlineMode ?? isBrowser;
|
|
495
|
+
this.enablePollingFallback = options.enablePollingFallback ?? true;
|
|
496
|
+
this.pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
497
|
+
this.maxSseRetries = options.maxSseRetries ?? DEFAULT_MAX_SSE_RETRIES;
|
|
498
|
+
const persistCache = options.persistCache ?? isBrowser;
|
|
499
|
+
this.browserCache = persistCache ? new BrowserCache() : null;
|
|
500
|
+
if (typeof navigator !== "undefined") {
|
|
501
|
+
this._isOnline = navigator.onLine;
|
|
502
|
+
}
|
|
219
503
|
const headers = [
|
|
220
504
|
["X-API-Key", this.apiKey],
|
|
221
505
|
["X-Flipswitch-SDK", this.getTelemetrySdkHeader()],
|
|
@@ -226,11 +510,13 @@ var FlipswitchProvider = class {
|
|
|
226
510
|
this.ofrepProvider = new OFREPWebProvider({
|
|
227
511
|
baseUrl: this.baseUrl,
|
|
228
512
|
fetchImplementation: this.fetchImpl,
|
|
229
|
-
headers
|
|
513
|
+
headers,
|
|
514
|
+
pollInterval: 0
|
|
515
|
+
// Disable polling - SSE handles real-time updates
|
|
230
516
|
});
|
|
231
517
|
}
|
|
232
518
|
getTelemetrySdkHeader() {
|
|
233
|
-
return `javascript/${
|
|
519
|
+
return `javascript/${version}`;
|
|
234
520
|
}
|
|
235
521
|
getTelemetryRuntimeHeader() {
|
|
236
522
|
if (typeof process !== "undefined" && process.versions?.node) {
|
|
@@ -296,6 +582,13 @@ var FlipswitchProvider = class {
|
|
|
296
582
|
*/
|
|
297
583
|
async initialize(context) {
|
|
298
584
|
this._status = ClientProviderStatus.NOT_READY;
|
|
585
|
+
this.setupOfflineHandling();
|
|
586
|
+
if (!this._isOnline && this.offlineMode) {
|
|
587
|
+
console.warn("[Flipswitch] Starting in offline mode - using cached flag values");
|
|
588
|
+
this._status = ClientProviderStatus.STALE;
|
|
589
|
+
this.emit(ClientProviderEvents.Stale);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
299
592
|
try {
|
|
300
593
|
await this.ofrepProvider.initialize(context);
|
|
301
594
|
} catch (error) {
|
|
@@ -330,15 +623,107 @@ var FlipswitchProvider = class {
|
|
|
330
623
|
this._status = ClientProviderStatus.READY;
|
|
331
624
|
this.emit(ClientProviderEvents.Ready);
|
|
332
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Setup online/offline event handling.
|
|
628
|
+
*/
|
|
629
|
+
setupOfflineHandling() {
|
|
630
|
+
if (typeof window === "undefined" || !this.offlineMode) return;
|
|
631
|
+
this.onlineHandler = () => {
|
|
632
|
+
this._isOnline = true;
|
|
633
|
+
console.info("[Flipswitch] Connection restored - refreshing flags");
|
|
634
|
+
if (this.enableRealtime && this.sseClient) {
|
|
635
|
+
this.sseClient.resume();
|
|
636
|
+
}
|
|
637
|
+
this.refreshFlags();
|
|
638
|
+
};
|
|
639
|
+
this.offlineHandler = () => {
|
|
640
|
+
this._isOnline = false;
|
|
641
|
+
console.warn("[Flipswitch] Connection lost - serving cached values");
|
|
642
|
+
if (this.sseClient) {
|
|
643
|
+
this.sseClient.pause();
|
|
644
|
+
}
|
|
645
|
+
this.stopPolling();
|
|
646
|
+
if (this._status !== ClientProviderStatus.STALE) {
|
|
647
|
+
this._status = ClientProviderStatus.STALE;
|
|
648
|
+
this.emit(ClientProviderEvents.Stale);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
window.addEventListener("online", this.onlineHandler);
|
|
652
|
+
window.addEventListener("offline", this.offlineHandler);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Refresh flags from the server.
|
|
656
|
+
*/
|
|
657
|
+
async refreshFlags() {
|
|
658
|
+
try {
|
|
659
|
+
await this.ofrepProvider.onContextChange?.({}, {});
|
|
660
|
+
if (this._status === ClientProviderStatus.STALE) {
|
|
661
|
+
this._status = ClientProviderStatus.READY;
|
|
662
|
+
this.emit(ClientProviderEvents.Ready);
|
|
663
|
+
}
|
|
664
|
+
this.emit(ClientProviderEvents.ConfigurationChanged);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
console.warn("[Flipswitch] Failed to refresh flags:", error);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
333
669
|
/**
|
|
334
670
|
* Called when the provider is shut down.
|
|
335
671
|
*/
|
|
336
672
|
async onClose() {
|
|
337
673
|
this.sseClient?.close();
|
|
338
674
|
this.sseClient = null;
|
|
675
|
+
this.stopPolling();
|
|
676
|
+
if (typeof window !== "undefined") {
|
|
677
|
+
if (this.onlineHandler) {
|
|
678
|
+
window.removeEventListener("online", this.onlineHandler);
|
|
679
|
+
this.onlineHandler = null;
|
|
680
|
+
}
|
|
681
|
+
if (this.offlineHandler) {
|
|
682
|
+
window.removeEventListener("offline", this.offlineHandler);
|
|
683
|
+
this.offlineHandler = null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
339
686
|
await this.ofrepProvider.onClose?.();
|
|
340
687
|
this._status = ClientProviderStatus.NOT_READY;
|
|
341
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Stop the polling fallback.
|
|
691
|
+
*/
|
|
692
|
+
stopPolling() {
|
|
693
|
+
if (this.pollingTimer) {
|
|
694
|
+
clearInterval(this.pollingTimer);
|
|
695
|
+
this.pollingTimer = null;
|
|
696
|
+
this.isPollingFallbackActive = false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Start polling fallback when SSE fails.
|
|
701
|
+
*/
|
|
702
|
+
startPollingFallback() {
|
|
703
|
+
if (this.isPollingFallbackActive || !this.enablePollingFallback) return;
|
|
704
|
+
console.info(`[Flipswitch] Starting polling fallback (interval: ${this.pollingInterval}ms)`);
|
|
705
|
+
this.isPollingFallbackActive = true;
|
|
706
|
+
this.pollingTimer = setInterval(async () => {
|
|
707
|
+
if (!this._isOnline) return;
|
|
708
|
+
try {
|
|
709
|
+
await this.refreshFlags();
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.warn("[Flipswitch] Polling refresh failed:", error);
|
|
712
|
+
}
|
|
713
|
+
}, this.pollingInterval);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Check if the provider is currently online.
|
|
717
|
+
*/
|
|
718
|
+
isOnline() {
|
|
719
|
+
return this._isOnline;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Check if polling fallback is active.
|
|
723
|
+
*/
|
|
724
|
+
isPollingActive() {
|
|
725
|
+
return this.isPollingFallbackActive;
|
|
726
|
+
}
|
|
342
727
|
/**
|
|
343
728
|
* Start the SSE connection for real-time updates.
|
|
344
729
|
*/
|
|
@@ -353,14 +738,27 @@ var FlipswitchProvider = class {
|
|
|
353
738
|
(status) => {
|
|
354
739
|
this.userEventHandlers.onConnectionStatusChange?.(status);
|
|
355
740
|
if (status === "error") {
|
|
741
|
+
this.sseRetryCount++;
|
|
742
|
+
if (this.sseRetryCount >= this.maxSseRetries && this.enablePollingFallback) {
|
|
743
|
+
console.warn(`[Flipswitch] SSE failed after ${this.sseRetryCount} retries - falling back to polling`);
|
|
744
|
+
this.startPollingFallback();
|
|
745
|
+
}
|
|
356
746
|
this._status = ClientProviderStatus.STALE;
|
|
357
747
|
this.emit(ClientProviderEvents.Stale);
|
|
358
|
-
} else if (status === "connected"
|
|
359
|
-
this.
|
|
360
|
-
this.
|
|
748
|
+
} else if (status === "connected") {
|
|
749
|
+
this.sseRetryCount = 0;
|
|
750
|
+
if (this.isPollingFallbackActive) {
|
|
751
|
+
console.info("[Flipswitch] SSE reconnected - stopping polling fallback");
|
|
752
|
+
this.stopPolling();
|
|
753
|
+
}
|
|
754
|
+
if (this._status === ClientProviderStatus.STALE) {
|
|
755
|
+
this._status = ClientProviderStatus.READY;
|
|
756
|
+
this.emit(ClientProviderEvents.Ready);
|
|
757
|
+
}
|
|
361
758
|
}
|
|
362
759
|
},
|
|
363
|
-
telemetryHeaders
|
|
760
|
+
telemetryHeaders,
|
|
761
|
+
this.enableVisibilityHandling
|
|
364
762
|
);
|
|
365
763
|
this.sseClient.connect();
|
|
366
764
|
}
|
|
@@ -377,10 +775,22 @@ var FlipswitchProvider = class {
|
|
|
377
775
|
}
|
|
378
776
|
/**
|
|
379
777
|
* Handle a flag change event from SSE.
|
|
380
|
-
*
|
|
778
|
+
* Triggers OFREP cache refresh, updates browser cache, and emits PROVIDER_CONFIGURATION_CHANGED.
|
|
381
779
|
*/
|
|
382
|
-
handleFlagChange(event) {
|
|
780
|
+
async handleFlagChange(event) {
|
|
383
781
|
this.userEventHandlers.onFlagChange?.(event);
|
|
782
|
+
if (this.browserCache) {
|
|
783
|
+
if (event.flagKey) {
|
|
784
|
+
this.browserCache.invalidate(event.flagKey);
|
|
785
|
+
} else {
|
|
786
|
+
this.browserCache.invalidate();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
await this.ofrepProvider.onContextChange?.({}, {});
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.warn("[Flipswitch] Failed to refresh flags after SSE event:", error);
|
|
793
|
+
}
|
|
384
794
|
this.emit(ClientProviderEvents.ConfigurationChanged);
|
|
385
795
|
}
|
|
386
796
|
/**
|
|
@@ -629,6 +1039,7 @@ var FlagCache = class {
|
|
|
629
1039
|
}
|
|
630
1040
|
};
|
|
631
1041
|
export {
|
|
1042
|
+
BrowserCache,
|
|
632
1043
|
FlagCache,
|
|
633
1044
|
FlipswitchProvider,
|
|
634
1045
|
SseClient
|