@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/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.error("[Flipswitch] Failed to establish SSE connection:", error);
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.error("[Flipswitch] SSE stream error:", error);
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.error("[Flipswitch] SSE connection error:", error);
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
- if (eventType === "flag-change") {
153
- try {
154
- const event = JSON.parse(data);
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
- } catch (error) {
157
- console.error("[Flipswitch] Failed to parse flag-change event:", error);
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.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 headers = [["X-API-Key", this.apiKey]];
226
- if (this.enableTelemetry) {
227
- headers.push(["X-Flipswitch-SDK", this.getTelemetrySdkHeader()]);
228
- headers.push(["X-Flipswitch-Runtime", this.getTelemetryRuntimeHeader()]);
229
- headers.push(["X-Flipswitch-OS", this.getTelemetryOsHeader()]);
230
- headers.push(["X-Flipswitch-Features", this.getTelemetryFeaturesHeader()]);
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" && this._status === import_core.ClientProviderStatus.STALE) {
366
- this._status = import_core.ClientProviderStatus.READY;
367
- this.emit(import_core.ClientProviderEvents.Ready);
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
- * Emits PROVIDER_CONFIGURATION_CHANGED to trigger re-evaluation.
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