@flipswitch-io/sdk 0.1.4 → 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 +228 -15
- package/dist/index.d.ts +228 -15
- package/dist/index.js +455 -30
- package/dist/index.mjs +454 -30
- package/package.json +2 -2
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
|
}
|
|
@@ -149,13 +213,34 @@ var SseClient = class {
|
|
|
149
213
|
if (eventType === "heartbeat") {
|
|
150
214
|
return;
|
|
151
215
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
216
|
+
try {
|
|
217
|
+
if (eventType === "flag-updated") {
|
|
218
|
+
const parsed = JSON.parse(data);
|
|
219
|
+
const event = {
|
|
220
|
+
flagKey: parsed.flagKey,
|
|
221
|
+
timestamp: parsed.timestamp
|
|
222
|
+
};
|
|
155
223
|
this.onFlagChange(event);
|
|
156
|
-
}
|
|
157
|
-
|
|
224
|
+
} else if (eventType === "config-updated") {
|
|
225
|
+
const parsed = JSON.parse(data);
|
|
226
|
+
const event = {
|
|
227
|
+
flagKey: null,
|
|
228
|
+
// null indicates all flags should be refreshed
|
|
229
|
+
timestamp: parsed.timestamp
|
|
230
|
+
};
|
|
231
|
+
this.onFlagChange(event);
|
|
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
|
+
}
|
|
158
241
|
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(`[Flipswitch] Failed to parse ${eventType} event:`, error);
|
|
159
244
|
}
|
|
160
245
|
}
|
|
161
246
|
/**
|
|
@@ -192,6 +277,14 @@ var SseClient = class {
|
|
|
192
277
|
close() {
|
|
193
278
|
this.closed = true;
|
|
194
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
|
+
}
|
|
195
288
|
if (this.reconnectTimeout) {
|
|
196
289
|
clearTimeout(this.reconnectTimeout);
|
|
197
290
|
this.reconnectTimeout = null;
|
|
@@ -203,9 +296,202 @@ var SseClient = class {
|
|
|
203
296
|
}
|
|
204
297
|
};
|
|
205
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
|
+
|
|
206
490
|
// src/provider.ts
|
|
207
491
|
var DEFAULT_BASE_URL = "https://api.flipswitch.io";
|
|
208
|
-
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;
|
|
209
495
|
var FlipswitchProvider = class {
|
|
210
496
|
constructor(options, eventHandlers) {
|
|
211
497
|
this.metadata = {
|
|
@@ -216,23 +502,41 @@ var FlipswitchProvider = class {
|
|
|
216
502
|
this._status = import_core.ClientProviderStatus.NOT_READY;
|
|
217
503
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
218
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;
|
|
219
511
|
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
220
512
|
this.apiKey = options.apiKey;
|
|
221
513
|
this.enableRealtime = options.enableRealtime ?? true;
|
|
222
|
-
this.enableTelemetry = options.enableTelemetry ?? true;
|
|
223
514
|
this.fetchImpl = options.fetchImplementation ?? (typeof window !== "undefined" ? fetch.bind(window) : fetch);
|
|
224
515
|
this.userEventHandlers = eventHandlers ?? {};
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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;
|
|
231
526
|
}
|
|
527
|
+
const headers = [
|
|
528
|
+
["X-API-Key", this.apiKey],
|
|
529
|
+
["X-Flipswitch-SDK", this.getTelemetrySdkHeader()],
|
|
530
|
+
["X-Flipswitch-Runtime", this.getTelemetryRuntimeHeader()],
|
|
531
|
+
["X-Flipswitch-OS", this.getTelemetryOsHeader()],
|
|
532
|
+
["X-Flipswitch-Features", this.getTelemetryFeaturesHeader()]
|
|
533
|
+
];
|
|
232
534
|
this.ofrepProvider = new import_ofrep_web_provider.OFREPWebProvider({
|
|
233
535
|
baseUrl: this.baseUrl,
|
|
234
536
|
fetchImplementation: this.fetchImpl,
|
|
235
|
-
headers
|
|
537
|
+
headers,
|
|
538
|
+
pollInterval: 0
|
|
539
|
+
// Disable polling - SSE handles real-time updates
|
|
236
540
|
});
|
|
237
541
|
}
|
|
238
542
|
getTelemetrySdkHeader() {
|
|
@@ -286,7 +590,6 @@ var FlipswitchProvider = class {
|
|
|
286
590
|
return `sse=${this.enableRealtime}`;
|
|
287
591
|
}
|
|
288
592
|
getTelemetryHeaders() {
|
|
289
|
-
if (!this.enableTelemetry) return {};
|
|
290
593
|
return {
|
|
291
594
|
"X-Flipswitch-SDK": this.getTelemetrySdkHeader(),
|
|
292
595
|
"X-Flipswitch-Runtime": this.getTelemetryRuntimeHeader(),
|
|
@@ -303,6 +606,13 @@ var FlipswitchProvider = class {
|
|
|
303
606
|
*/
|
|
304
607
|
async initialize(context) {
|
|
305
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
|
+
}
|
|
306
616
|
try {
|
|
307
617
|
await this.ofrepProvider.initialize(context);
|
|
308
618
|
} catch (error) {
|
|
@@ -337,15 +647,107 @@ var FlipswitchProvider = class {
|
|
|
337
647
|
this._status = import_core.ClientProviderStatus.READY;
|
|
338
648
|
this.emit(import_core.ClientProviderEvents.Ready);
|
|
339
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
|
+
}
|
|
340
693
|
/**
|
|
341
694
|
* Called when the provider is shut down.
|
|
342
695
|
*/
|
|
343
696
|
async onClose() {
|
|
344
697
|
this.sseClient?.close();
|
|
345
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
|
+
}
|
|
346
710
|
await this.ofrepProvider.onClose?.();
|
|
347
711
|
this._status = import_core.ClientProviderStatus.NOT_READY;
|
|
348
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
|
+
}
|
|
349
751
|
/**
|
|
350
752
|
* Start the SSE connection for real-time updates.
|
|
351
753
|
*/
|
|
@@ -360,14 +762,27 @@ var FlipswitchProvider = class {
|
|
|
360
762
|
(status) => {
|
|
361
763
|
this.userEventHandlers.onConnectionStatusChange?.(status);
|
|
362
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
|
+
}
|
|
363
770
|
this._status = import_core.ClientProviderStatus.STALE;
|
|
364
771
|
this.emit(import_core.ClientProviderEvents.Stale);
|
|
365
|
-
} else if (status === "connected"
|
|
366
|
-
this.
|
|
367
|
-
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
|
+
}
|
|
368
782
|
}
|
|
369
783
|
},
|
|
370
|
-
telemetryHeaders
|
|
784
|
+
telemetryHeaders,
|
|
785
|
+
this.enableVisibilityHandling
|
|
371
786
|
);
|
|
372
787
|
this.sseClient.connect();
|
|
373
788
|
}
|
|
@@ -375,9 +790,6 @@ var FlipswitchProvider = class {
|
|
|
375
790
|
* Get telemetry headers as a map.
|
|
376
791
|
*/
|
|
377
792
|
getTelemetryHeadersMap() {
|
|
378
|
-
if (!this.enableTelemetry) {
|
|
379
|
-
return void 0;
|
|
380
|
-
}
|
|
381
793
|
return {
|
|
382
794
|
"X-Flipswitch-SDK": this.getTelemetrySdkHeader(),
|
|
383
795
|
"X-Flipswitch-Runtime": this.getTelemetryRuntimeHeader(),
|
|
@@ -387,10 +799,22 @@ var FlipswitchProvider = class {
|
|
|
387
799
|
}
|
|
388
800
|
/**
|
|
389
801
|
* Handle a flag change event from SSE.
|
|
390
|
-
*
|
|
802
|
+
* Triggers OFREP cache refresh, updates browser cache, and emits PROVIDER_CONFIGURATION_CHANGED.
|
|
391
803
|
*/
|
|
392
|
-
handleFlagChange(event) {
|
|
804
|
+
async handleFlagChange(event) {
|
|
393
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
|
+
}
|
|
394
818
|
this.emit(import_core.ClientProviderEvents.ConfigurationChanged);
|
|
395
819
|
}
|
|
396
820
|
/**
|
|
@@ -640,6 +1064,7 @@ var FlagCache = class {
|
|
|
640
1064
|
};
|
|
641
1065
|
// Annotate the CommonJS export names for ESM import in node:
|
|
642
1066
|
0 && (module.exports = {
|
|
1067
|
+
BrowserCache,
|
|
643
1068
|
FlagCache,
|
|
644
1069
|
FlipswitchProvider,
|
|
645
1070
|
SseClient
|