@flipswitch-io/sdk 0.1.5 → 0.1.6
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 +432 -22
- package/dist/index.mjs +431 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BrowserCache: () => BrowserCache,
|
|
23
24
|
FlagCache: () => FlagCache,
|
|
24
25
|
FlipswitchProvider: () => FlipswitchProvider,
|
|
25
26
|
SseClient: () => SseClient
|
|
@@ -34,26 +35,37 @@ var import_ofrep_web_provider = require("@openfeature/ofrep-web-provider");
|
|
|
34
35
|
var MIN_RETRY_DELAY = 1e3;
|
|
35
36
|
var MAX_RETRY_DELAY = 3e4;
|
|
36
37
|
var SseClient = class {
|
|
37
|
-
constructor(baseUrl, apiKey, onFlagChange, onStatusChange, telemetryHeaders) {
|
|
38
|
+
constructor(baseUrl, apiKey, onFlagChange, onStatusChange, telemetryHeaders, enableVisibilityHandling = true) {
|
|
38
39
|
this.baseUrl = baseUrl;
|
|
39
40
|
this.apiKey = apiKey;
|
|
40
41
|
this.onFlagChange = onFlagChange;
|
|
41
42
|
this.onStatusChange = onStatusChange;
|
|
42
43
|
this.telemetryHeaders = telemetryHeaders;
|
|
44
|
+
this.enableVisibilityHandling = enableVisibilityHandling;
|
|
43
45
|
this.eventSource = null;
|
|
44
46
|
this.retryDelay = MIN_RETRY_DELAY;
|
|
45
47
|
this.reconnectTimeout = null;
|
|
46
48
|
this.closed = false;
|
|
49
|
+
this.paused = false;
|
|
47
50
|
this.status = "disconnected";
|
|
51
|
+
this.visibilityHandler = null;
|
|
52
|
+
this.abortController = null;
|
|
48
53
|
}
|
|
49
54
|
/**
|
|
50
55
|
* Start the SSE connection.
|
|
51
56
|
*/
|
|
52
57
|
connect() {
|
|
53
|
-
if (this.closed) return;
|
|
58
|
+
if (this.closed || this.paused) return;
|
|
59
|
+
if (this.abortController) {
|
|
60
|
+
this.abortController.abort();
|
|
61
|
+
}
|
|
62
|
+
this.abortController = new AbortController();
|
|
54
63
|
if (this.eventSource) {
|
|
55
64
|
this.eventSource.close();
|
|
56
65
|
}
|
|
66
|
+
if (this.enableVisibilityHandling) {
|
|
67
|
+
this.setupVisibilityHandling();
|
|
68
|
+
}
|
|
57
69
|
this.updateStatus("connecting");
|
|
58
70
|
const url = `${this.baseUrl}/api/v1/flags/events`;
|
|
59
71
|
try {
|
|
@@ -63,11 +75,62 @@ var SseClient = class {
|
|
|
63
75
|
this.connectWithPolyfill(url);
|
|
64
76
|
}
|
|
65
77
|
} catch (error) {
|
|
66
|
-
console.
|
|
78
|
+
console.warn("[Flipswitch] Failed to establish SSE connection:", error);
|
|
67
79
|
this.updateStatus("error");
|
|
68
80
|
this.scheduleReconnect();
|
|
69
81
|
}
|
|
70
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Setup visibility API handling.
|
|
85
|
+
* Disconnects when tab is hidden, reconnects when visible.
|
|
86
|
+
*/
|
|
87
|
+
setupVisibilityHandling() {
|
|
88
|
+
if (typeof document === "undefined" || this.visibilityHandler) return;
|
|
89
|
+
this.visibilityHandler = () => {
|
|
90
|
+
if (document.hidden) {
|
|
91
|
+
this.pause();
|
|
92
|
+
} else {
|
|
93
|
+
this.resume();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Pause the SSE connection (used by visibility API).
|
|
100
|
+
* Different from close() - can be resumed.
|
|
101
|
+
*/
|
|
102
|
+
pause() {
|
|
103
|
+
if (this.paused || this.closed) return;
|
|
104
|
+
this.paused = true;
|
|
105
|
+
if (this.abortController) {
|
|
106
|
+
this.abortController.abort();
|
|
107
|
+
this.abortController = null;
|
|
108
|
+
}
|
|
109
|
+
if (this.reconnectTimeout) {
|
|
110
|
+
clearTimeout(this.reconnectTimeout);
|
|
111
|
+
this.reconnectTimeout = null;
|
|
112
|
+
}
|
|
113
|
+
if (this.eventSource) {
|
|
114
|
+
this.eventSource.close();
|
|
115
|
+
this.eventSource = null;
|
|
116
|
+
}
|
|
117
|
+
this.updateStatus("disconnected");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resume the SSE connection after being paused.
|
|
121
|
+
*/
|
|
122
|
+
resume() {
|
|
123
|
+
if (!this.paused || this.closed) return;
|
|
124
|
+
this.paused = false;
|
|
125
|
+
this.retryDelay = MIN_RETRY_DELAY;
|
|
126
|
+
this.connect();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if the connection is paused.
|
|
130
|
+
*/
|
|
131
|
+
isPaused() {
|
|
132
|
+
return this.paused;
|
|
133
|
+
}
|
|
71
134
|
/**
|
|
72
135
|
* Connect using fetch-based SSE (supports custom headers).
|
|
73
136
|
*/
|
|
@@ -81,7 +144,8 @@ var SseClient = class {
|
|
|
81
144
|
};
|
|
82
145
|
const response = await fetch(url, {
|
|
83
146
|
method: "GET",
|
|
84
|
-
headers
|
|
147
|
+
headers,
|
|
148
|
+
signal: this.abortController?.signal
|
|
85
149
|
});
|
|
86
150
|
if (!response.ok) {
|
|
87
151
|
throw new Error(`SSE connection failed: ${response.status}`);
|
|
@@ -122,14 +186,14 @@ var SseClient = class {
|
|
|
122
186
|
};
|
|
123
187
|
processStream().catch((error) => {
|
|
124
188
|
if (!this.closed) {
|
|
125
|
-
console.
|
|
189
|
+
console.warn("[Flipswitch] SSE stream error:", error);
|
|
126
190
|
this.updateStatus("error");
|
|
127
191
|
this.scheduleReconnect();
|
|
128
192
|
}
|
|
129
193
|
});
|
|
130
194
|
} catch (error) {
|
|
131
195
|
if (!this.closed) {
|
|
132
|
-
console.
|
|
196
|
+
console.warn("[Flipswitch] SSE connection error:", error);
|
|
133
197
|
this.updateStatus("error");
|
|
134
198
|
this.scheduleReconnect();
|
|
135
199
|
}
|
|
@@ -159,20 +223,21 @@ var SseClient = class {
|
|
|
159
223
|
this.onFlagChange(event);
|
|
160
224
|
} else if (eventType === "config-updated") {
|
|
161
225
|
const parsed = JSON.parse(data);
|
|
162
|
-
if (parsed.reason === "api-key-rotated") {
|
|
163
|
-
console.warn(
|
|
164
|
-
"[Flipswitch] API key has been rotated. You may need to update your API key configuration."
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
226
|
const event = {
|
|
168
227
|
flagKey: null,
|
|
169
228
|
// null indicates all flags should be refreshed
|
|
170
229
|
timestamp: parsed.timestamp
|
|
171
230
|
};
|
|
172
231
|
this.onFlagChange(event);
|
|
173
|
-
} else if (eventType === "
|
|
174
|
-
const
|
|
175
|
-
|
|
232
|
+
} else if (eventType === "api-key-rotated") {
|
|
233
|
+
const parsed = JSON.parse(data);
|
|
234
|
+
if (!parsed.validUntil) {
|
|
235
|
+
console.info("[Flipswitch] API key rotation was aborted");
|
|
236
|
+
} else {
|
|
237
|
+
console.warn(
|
|
238
|
+
`[Flipswitch] API key was rotated. Current key valid until: ${parsed.validUntil}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
176
241
|
}
|
|
177
242
|
} catch (error) {
|
|
178
243
|
console.error(`[Flipswitch] Failed to parse ${eventType} event:`, error);
|
|
@@ -212,6 +277,14 @@ var SseClient = class {
|
|
|
212
277
|
close() {
|
|
213
278
|
this.closed = true;
|
|
214
279
|
this.updateStatus("disconnected");
|
|
280
|
+
if (this.visibilityHandler && typeof document !== "undefined") {
|
|
281
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
282
|
+
this.visibilityHandler = null;
|
|
283
|
+
}
|
|
284
|
+
if (this.abortController) {
|
|
285
|
+
this.abortController.abort();
|
|
286
|
+
this.abortController = null;
|
|
287
|
+
}
|
|
215
288
|
if (this.reconnectTimeout) {
|
|
216
289
|
clearTimeout(this.reconnectTimeout);
|
|
217
290
|
this.reconnectTimeout = null;
|
|
@@ -223,9 +296,202 @@ var SseClient = class {
|
|
|
223
296
|
}
|
|
224
297
|
};
|
|
225
298
|
|
|
299
|
+
// src/browser-cache.ts
|
|
300
|
+
var CACHE_PREFIX = "flipswitch:flags:";
|
|
301
|
+
var CACHE_META_KEY = "flipswitch:cache-meta";
|
|
302
|
+
var BrowserCache = class {
|
|
303
|
+
constructor() {
|
|
304
|
+
this.cacheVersion = 1;
|
|
305
|
+
this.storage = this.getStorage();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get storage instance if available.
|
|
309
|
+
*/
|
|
310
|
+
getStorage() {
|
|
311
|
+
if (typeof window === "undefined") return null;
|
|
312
|
+
try {
|
|
313
|
+
const testKey = "__flipswitch_test__";
|
|
314
|
+
window.localStorage.setItem(testKey, "test");
|
|
315
|
+
window.localStorage.removeItem(testKey);
|
|
316
|
+
return window.localStorage;
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check if browser cache is available.
|
|
323
|
+
*/
|
|
324
|
+
isAvailable() {
|
|
325
|
+
return this.storage !== null;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get a cached flag value.
|
|
329
|
+
*/
|
|
330
|
+
get(flagKey) {
|
|
331
|
+
if (!this.storage) return null;
|
|
332
|
+
try {
|
|
333
|
+
const key = CACHE_PREFIX + flagKey;
|
|
334
|
+
const item = this.storage.getItem(key);
|
|
335
|
+
if (!item) return null;
|
|
336
|
+
const cached = JSON.parse(item);
|
|
337
|
+
return cached;
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Set a cached flag value.
|
|
344
|
+
*/
|
|
345
|
+
set(flagKey, value, variant, reason) {
|
|
346
|
+
if (!this.storage) return;
|
|
347
|
+
try {
|
|
348
|
+
const key = CACHE_PREFIX + flagKey;
|
|
349
|
+
const cached = {
|
|
350
|
+
value,
|
|
351
|
+
timestamp: Date.now(),
|
|
352
|
+
variant,
|
|
353
|
+
reason
|
|
354
|
+
};
|
|
355
|
+
this.storage.setItem(key, JSON.stringify(cached));
|
|
356
|
+
this.updateMeta();
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Set multiple cached flag values at once.
|
|
362
|
+
*/
|
|
363
|
+
setAll(flags) {
|
|
364
|
+
if (!this.storage) return;
|
|
365
|
+
try {
|
|
366
|
+
for (const flag of flags) {
|
|
367
|
+
const cacheKey = CACHE_PREFIX + flag.key;
|
|
368
|
+
const cached = {
|
|
369
|
+
value: flag.value,
|
|
370
|
+
timestamp: Date.now(),
|
|
371
|
+
variant: flag.variant,
|
|
372
|
+
reason: flag.reason
|
|
373
|
+
};
|
|
374
|
+
this.storage.setItem(cacheKey, JSON.stringify(cached));
|
|
375
|
+
}
|
|
376
|
+
this.updateMeta();
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Get all cached flags.
|
|
382
|
+
*/
|
|
383
|
+
getAll() {
|
|
384
|
+
const result = /* @__PURE__ */ new Map();
|
|
385
|
+
if (!this.storage) return result;
|
|
386
|
+
try {
|
|
387
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
388
|
+
const key = this.storage.key(i);
|
|
389
|
+
if (key?.startsWith(CACHE_PREFIX)) {
|
|
390
|
+
const flagKey = key.slice(CACHE_PREFIX.length);
|
|
391
|
+
const item = this.storage.getItem(key);
|
|
392
|
+
if (item) {
|
|
393
|
+
const cached = JSON.parse(item);
|
|
394
|
+
result.set(flagKey, cached);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Invalidate a specific flag or all flags.
|
|
404
|
+
*/
|
|
405
|
+
invalidate(flagKey) {
|
|
406
|
+
if (!this.storage) return;
|
|
407
|
+
try {
|
|
408
|
+
if (flagKey) {
|
|
409
|
+
this.storage.removeItem(CACHE_PREFIX + flagKey);
|
|
410
|
+
} else {
|
|
411
|
+
const keysToRemove = [];
|
|
412
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
413
|
+
const key = this.storage.key(i);
|
|
414
|
+
if (key?.startsWith(CACHE_PREFIX)) {
|
|
415
|
+
keysToRemove.push(key);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const key of keysToRemove) {
|
|
419
|
+
this.storage.removeItem(key);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
this.updateMeta();
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Update cache metadata.
|
|
428
|
+
*/
|
|
429
|
+
updateMeta() {
|
|
430
|
+
if (!this.storage) return;
|
|
431
|
+
try {
|
|
432
|
+
const meta = {
|
|
433
|
+
lastUpdated: Date.now(),
|
|
434
|
+
version: this.cacheVersion
|
|
435
|
+
};
|
|
436
|
+
this.storage.setItem(CACHE_META_KEY, JSON.stringify(meta));
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get cache metadata.
|
|
442
|
+
*/
|
|
443
|
+
getMeta() {
|
|
444
|
+
if (!this.storage) return null;
|
|
445
|
+
try {
|
|
446
|
+
const item = this.storage.getItem(CACHE_META_KEY);
|
|
447
|
+
if (!item) return null;
|
|
448
|
+
return JSON.parse(item);
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Get the age of a cached flag in milliseconds.
|
|
455
|
+
*/
|
|
456
|
+
getAge(flagKey) {
|
|
457
|
+
const cached = this.get(flagKey);
|
|
458
|
+
if (!cached) return null;
|
|
459
|
+
return Date.now() - cached.timestamp;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Check if a cached flag is stale based on max age.
|
|
463
|
+
*/
|
|
464
|
+
isStale(flagKey, maxAgeMs) {
|
|
465
|
+
const age = this.getAge(flagKey);
|
|
466
|
+
if (age === null) return true;
|
|
467
|
+
return age > maxAgeMs;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Clear all Flipswitch data from localStorage.
|
|
471
|
+
*/
|
|
472
|
+
clear() {
|
|
473
|
+
if (!this.storage) return;
|
|
474
|
+
try {
|
|
475
|
+
const keysToRemove = [];
|
|
476
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
477
|
+
const key = this.storage.key(i);
|
|
478
|
+
if (key?.startsWith("flipswitch:")) {
|
|
479
|
+
keysToRemove.push(key);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const key of keysToRemove) {
|
|
483
|
+
this.storage.removeItem(key);
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
226
490
|
// src/provider.ts
|
|
227
491
|
var DEFAULT_BASE_URL = "https://api.flipswitch.io";
|
|
228
|
-
var SDK_VERSION = "0.1.
|
|
492
|
+
var SDK_VERSION = "0.1.2";
|
|
493
|
+
var DEFAULT_POLLING_INTERVAL = 3e4;
|
|
494
|
+
var DEFAULT_MAX_SSE_RETRIES = 5;
|
|
229
495
|
var FlipswitchProvider = class {
|
|
230
496
|
constructor(options, eventHandlers) {
|
|
231
497
|
this.metadata = {
|
|
@@ -236,11 +502,28 @@ var FlipswitchProvider = class {
|
|
|
236
502
|
this._status = import_core.ClientProviderStatus.NOT_READY;
|
|
237
503
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
238
504
|
this.userEventHandlers = {};
|
|
505
|
+
this.pollingTimer = null;
|
|
506
|
+
this.sseRetryCount = 0;
|
|
507
|
+
this.isPollingFallbackActive = false;
|
|
508
|
+
this.onlineHandler = null;
|
|
509
|
+
this.offlineHandler = null;
|
|
510
|
+
this._isOnline = true;
|
|
239
511
|
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
240
512
|
this.apiKey = options.apiKey;
|
|
241
513
|
this.enableRealtime = options.enableRealtime ?? true;
|
|
242
514
|
this.fetchImpl = options.fetchImplementation ?? (typeof window !== "undefined" ? fetch.bind(window) : fetch);
|
|
243
515
|
this.userEventHandlers = eventHandlers ?? {};
|
|
516
|
+
const isBrowser = typeof window !== "undefined";
|
|
517
|
+
this.enableVisibilityHandling = options.enableVisibilityHandling ?? isBrowser;
|
|
518
|
+
this.offlineMode = options.offlineMode ?? isBrowser;
|
|
519
|
+
this.enablePollingFallback = options.enablePollingFallback ?? true;
|
|
520
|
+
this.pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
521
|
+
this.maxSseRetries = options.maxSseRetries ?? DEFAULT_MAX_SSE_RETRIES;
|
|
522
|
+
const persistCache = options.persistCache ?? isBrowser;
|
|
523
|
+
this.browserCache = persistCache ? new BrowserCache() : null;
|
|
524
|
+
if (typeof navigator !== "undefined") {
|
|
525
|
+
this._isOnline = navigator.onLine;
|
|
526
|
+
}
|
|
244
527
|
const headers = [
|
|
245
528
|
["X-API-Key", this.apiKey],
|
|
246
529
|
["X-Flipswitch-SDK", this.getTelemetrySdkHeader()],
|
|
@@ -251,7 +534,9 @@ var FlipswitchProvider = class {
|
|
|
251
534
|
this.ofrepProvider = new import_ofrep_web_provider.OFREPWebProvider({
|
|
252
535
|
baseUrl: this.baseUrl,
|
|
253
536
|
fetchImplementation: this.fetchImpl,
|
|
254
|
-
headers
|
|
537
|
+
headers,
|
|
538
|
+
pollInterval: 0
|
|
539
|
+
// Disable polling - SSE handles real-time updates
|
|
255
540
|
});
|
|
256
541
|
}
|
|
257
542
|
getTelemetrySdkHeader() {
|
|
@@ -321,6 +606,13 @@ var FlipswitchProvider = class {
|
|
|
321
606
|
*/
|
|
322
607
|
async initialize(context) {
|
|
323
608
|
this._status = import_core.ClientProviderStatus.NOT_READY;
|
|
609
|
+
this.setupOfflineHandling();
|
|
610
|
+
if (!this._isOnline && this.offlineMode) {
|
|
611
|
+
console.warn("[Flipswitch] Starting in offline mode - using cached flag values");
|
|
612
|
+
this._status = import_core.ClientProviderStatus.STALE;
|
|
613
|
+
this.emit(import_core.ClientProviderEvents.Stale);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
324
616
|
try {
|
|
325
617
|
await this.ofrepProvider.initialize(context);
|
|
326
618
|
} catch (error) {
|
|
@@ -355,15 +647,107 @@ var FlipswitchProvider = class {
|
|
|
355
647
|
this._status = import_core.ClientProviderStatus.READY;
|
|
356
648
|
this.emit(import_core.ClientProviderEvents.Ready);
|
|
357
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Setup online/offline event handling.
|
|
652
|
+
*/
|
|
653
|
+
setupOfflineHandling() {
|
|
654
|
+
if (typeof window === "undefined" || !this.offlineMode) return;
|
|
655
|
+
this.onlineHandler = () => {
|
|
656
|
+
this._isOnline = true;
|
|
657
|
+
console.info("[Flipswitch] Connection restored - refreshing flags");
|
|
658
|
+
if (this.enableRealtime && this.sseClient) {
|
|
659
|
+
this.sseClient.resume();
|
|
660
|
+
}
|
|
661
|
+
this.refreshFlags();
|
|
662
|
+
};
|
|
663
|
+
this.offlineHandler = () => {
|
|
664
|
+
this._isOnline = false;
|
|
665
|
+
console.warn("[Flipswitch] Connection lost - serving cached values");
|
|
666
|
+
if (this.sseClient) {
|
|
667
|
+
this.sseClient.pause();
|
|
668
|
+
}
|
|
669
|
+
this.stopPolling();
|
|
670
|
+
if (this._status !== import_core.ClientProviderStatus.STALE) {
|
|
671
|
+
this._status = import_core.ClientProviderStatus.STALE;
|
|
672
|
+
this.emit(import_core.ClientProviderEvents.Stale);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
window.addEventListener("online", this.onlineHandler);
|
|
676
|
+
window.addEventListener("offline", this.offlineHandler);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Refresh flags from the server.
|
|
680
|
+
*/
|
|
681
|
+
async refreshFlags() {
|
|
682
|
+
try {
|
|
683
|
+
await this.ofrepProvider.onContextChange?.({}, {});
|
|
684
|
+
if (this._status === import_core.ClientProviderStatus.STALE) {
|
|
685
|
+
this._status = import_core.ClientProviderStatus.READY;
|
|
686
|
+
this.emit(import_core.ClientProviderEvents.Ready);
|
|
687
|
+
}
|
|
688
|
+
this.emit(import_core.ClientProviderEvents.ConfigurationChanged);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
console.warn("[Flipswitch] Failed to refresh flags:", error);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
358
693
|
/**
|
|
359
694
|
* Called when the provider is shut down.
|
|
360
695
|
*/
|
|
361
696
|
async onClose() {
|
|
362
697
|
this.sseClient?.close();
|
|
363
698
|
this.sseClient = null;
|
|
699
|
+
this.stopPolling();
|
|
700
|
+
if (typeof window !== "undefined") {
|
|
701
|
+
if (this.onlineHandler) {
|
|
702
|
+
window.removeEventListener("online", this.onlineHandler);
|
|
703
|
+
this.onlineHandler = null;
|
|
704
|
+
}
|
|
705
|
+
if (this.offlineHandler) {
|
|
706
|
+
window.removeEventListener("offline", this.offlineHandler);
|
|
707
|
+
this.offlineHandler = null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
364
710
|
await this.ofrepProvider.onClose?.();
|
|
365
711
|
this._status = import_core.ClientProviderStatus.NOT_READY;
|
|
366
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Stop the polling fallback.
|
|
715
|
+
*/
|
|
716
|
+
stopPolling() {
|
|
717
|
+
if (this.pollingTimer) {
|
|
718
|
+
clearInterval(this.pollingTimer);
|
|
719
|
+
this.pollingTimer = null;
|
|
720
|
+
this.isPollingFallbackActive = false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Start polling fallback when SSE fails.
|
|
725
|
+
*/
|
|
726
|
+
startPollingFallback() {
|
|
727
|
+
if (this.isPollingFallbackActive || !this.enablePollingFallback) return;
|
|
728
|
+
console.info(`[Flipswitch] Starting polling fallback (interval: ${this.pollingInterval}ms)`);
|
|
729
|
+
this.isPollingFallbackActive = true;
|
|
730
|
+
this.pollingTimer = setInterval(async () => {
|
|
731
|
+
if (!this._isOnline) return;
|
|
732
|
+
try {
|
|
733
|
+
await this.refreshFlags();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.warn("[Flipswitch] Polling refresh failed:", error);
|
|
736
|
+
}
|
|
737
|
+
}, this.pollingInterval);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Check if the provider is currently online.
|
|
741
|
+
*/
|
|
742
|
+
isOnline() {
|
|
743
|
+
return this._isOnline;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Check if polling fallback is active.
|
|
747
|
+
*/
|
|
748
|
+
isPollingActive() {
|
|
749
|
+
return this.isPollingFallbackActive;
|
|
750
|
+
}
|
|
367
751
|
/**
|
|
368
752
|
* Start the SSE connection for real-time updates.
|
|
369
753
|
*/
|
|
@@ -378,14 +762,27 @@ var FlipswitchProvider = class {
|
|
|
378
762
|
(status) => {
|
|
379
763
|
this.userEventHandlers.onConnectionStatusChange?.(status);
|
|
380
764
|
if (status === "error") {
|
|
765
|
+
this.sseRetryCount++;
|
|
766
|
+
if (this.sseRetryCount >= this.maxSseRetries && this.enablePollingFallback) {
|
|
767
|
+
console.warn(`[Flipswitch] SSE failed after ${this.sseRetryCount} retries - falling back to polling`);
|
|
768
|
+
this.startPollingFallback();
|
|
769
|
+
}
|
|
381
770
|
this._status = import_core.ClientProviderStatus.STALE;
|
|
382
771
|
this.emit(import_core.ClientProviderEvents.Stale);
|
|
383
|
-
} else if (status === "connected"
|
|
384
|
-
this.
|
|
385
|
-
this.
|
|
772
|
+
} else if (status === "connected") {
|
|
773
|
+
this.sseRetryCount = 0;
|
|
774
|
+
if (this.isPollingFallbackActive) {
|
|
775
|
+
console.info("[Flipswitch] SSE reconnected - stopping polling fallback");
|
|
776
|
+
this.stopPolling();
|
|
777
|
+
}
|
|
778
|
+
if (this._status === import_core.ClientProviderStatus.STALE) {
|
|
779
|
+
this._status = import_core.ClientProviderStatus.READY;
|
|
780
|
+
this.emit(import_core.ClientProviderEvents.Ready);
|
|
781
|
+
}
|
|
386
782
|
}
|
|
387
783
|
},
|
|
388
|
-
telemetryHeaders
|
|
784
|
+
telemetryHeaders,
|
|
785
|
+
this.enableVisibilityHandling
|
|
389
786
|
);
|
|
390
787
|
this.sseClient.connect();
|
|
391
788
|
}
|
|
@@ -402,10 +799,22 @@ var FlipswitchProvider = class {
|
|
|
402
799
|
}
|
|
403
800
|
/**
|
|
404
801
|
* Handle a flag change event from SSE.
|
|
405
|
-
*
|
|
802
|
+
* Triggers OFREP cache refresh, updates browser cache, and emits PROVIDER_CONFIGURATION_CHANGED.
|
|
406
803
|
*/
|
|
407
|
-
handleFlagChange(event) {
|
|
804
|
+
async handleFlagChange(event) {
|
|
408
805
|
this.userEventHandlers.onFlagChange?.(event);
|
|
806
|
+
if (this.browserCache) {
|
|
807
|
+
if (event.flagKey) {
|
|
808
|
+
this.browserCache.invalidate(event.flagKey);
|
|
809
|
+
} else {
|
|
810
|
+
this.browserCache.invalidate();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
await this.ofrepProvider.onContextChange?.({}, {});
|
|
815
|
+
} catch (error) {
|
|
816
|
+
console.warn("[Flipswitch] Failed to refresh flags after SSE event:", error);
|
|
817
|
+
}
|
|
409
818
|
this.emit(import_core.ClientProviderEvents.ConfigurationChanged);
|
|
410
819
|
}
|
|
411
820
|
/**
|
|
@@ -655,6 +1064,7 @@ var FlagCache = class {
|
|
|
655
1064
|
};
|
|
656
1065
|
// Annotate the CommonJS export names for ESM import in node:
|
|
657
1066
|
0 && (module.exports = {
|
|
1067
|
+
BrowserCache,
|
|
658
1068
|
FlagCache,
|
|
659
1069
|
FlipswitchProvider,
|
|
660
1070
|
SseClient
|