Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions libs/providers/ofrep-web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [Unreleased]

### ⚠️ BREAKING CHANGES

* **polling:** polling is now disabled by default (pollInterval defaults to 0). Polling can be enabled by explicitly setting `pollInterval` to a positive number (ms). This applies to OFREP providers which are still sub-v1.

### ✨ New Features

* **visibility-change:** automatically re-fetch flags when page/app becomes visible

## [0.3.6](https://github.com/open-feature/js-sdk-contrib/compare/ofrep-web-provider-v0.3.5...ofrep-web-provider-v0.3.6) (2026-03-27)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export type OFREPWebProviderOptions = OFREPProviderBaseOptions & {
* pollInterval is the time in milliseconds to wait between we call the OFREP
* API to get the latest evaluation of your flags.
*
* If a negative number is provided, the provider will not poll the OFREP API.
* Default: 30000
* If a negative number or 0 is provided, the provider will not poll the OFREP API.
* This is the default behavior. Polling is available as an opt-in configuration.
* Default: 0 (disabled)
Comment thread
marcozabel marked this conversation as resolved.
*/
pollInterval?: number; // in milliseconds
};
37 changes: 36 additions & 1 deletion libs/providers/ofrep-web/src/lib/ofrep-web-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import type { FlagCache, MetadataCache } from './model/in-memory-cache';
import type { OFREPWebProviderOptions } from './model/ofrep-web-provider-options';

export class OFREPWebProvider implements Provider {
DEFAULT_POLL_INTERVAL = 30000;
DEFAULT_POLL_INTERVAL = 0;

readonly metadata = {
name: 'OpenFeature Remote Evaluation Protocol Web Provider',
Expand All @@ -57,6 +57,7 @@ export class OFREPWebProvider implements Provider {
private _flagSetMetadataCache?: MetadataCache;
private _context: EvaluationContext | undefined;
private _pollingIntervalId?: number;
private _visibilityChangeHandler = this._onVisibilityChange.bind(this);

constructor(options: OFREPWebProviderOptions, logger?: Logger) {
this._options = options;
Expand Down Expand Up @@ -86,6 +87,11 @@ export class OFREPWebProvider implements Provider {
this.startPolling();
}

// Listen for page/app visibility changes to refetch flags when becoming visible
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', this._visibilityChangeHandler);
}
Copy link
Copy Markdown
Member

@MattIPv4 MattIPv4 Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this should be opt-in, or at least opt-out? Some folks may well want to load flags once on app start and ensure they don't change again?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this will be especially true in micro-frontend architectures where there may be many providers defined in different domains -- if they all refresh every time the page visiblity changes, that is a lot of network requests being fired off at once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukas-reining @MattIPv4 thanks for the suggestion, I added a config option for opt-in 👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you :)


this._logger?.debug(`${this.metadata.name} initialized successfully`);
} catch (error) {
if (error instanceof OFREPApiUnauthorizedError || error instanceof OFREPForbiddenError) {
Expand Down Expand Up @@ -166,6 +172,9 @@ export class OFREPWebProvider implements Provider {
*/
onClose?(): Promise<void> {
this.stopPolling();
if (typeof document !== 'undefined') {
document.removeEventListener('visibilitychange', this._visibilityChangeHandler);
}
this._ofrepAPI.close();
return Promise.resolve();
}
Expand Down Expand Up @@ -319,4 +328,30 @@ export class OFREPWebProvider implements Provider {
clearInterval(this._pollingIntervalId);
}
}

/**
* Handler for visibility changes (page/app becoming visible)
* Re-fetches flags when the document becomes visible
* @private
*/
private async _onVisibilityChange() {
// Only re-fetch when the page becomes visible, not when it's hidden
if (typeof document !== 'undefined' && document.visibilityState === 'visible') {
try {
const now = new Date();
if (this._retryPollingAfter !== undefined && this._retryPollingAfter > now) {
return;
}
const res = await this._fetchFlags(this._context);
if (res.status === BulkEvaluationStatus.SUCCESS_WITH_CHANGES) {
this.events?.emit(ClientProviderEvents.ConfigurationChanged, {
message: 'Flags updated after visibility change',
flagsChanged: res.flags,
});
}
} catch (error) {
this.events?.emit(ClientProviderEvents.Stale, { message: `Error while fetching after visibility change: ${error}` });
}
}
}
Comment thread
marcozabel marked this conversation as resolved.
Outdated
Comment thread
marcozabel marked this conversation as resolved.
Outdated
}
Loading