better-auth-feature-flags
Version:
Ship features safely with feature flags, A/B testing, and progressive rollouts - Better Auth plugin for modern release management
1,022 lines (1,017 loc) • 29.9 kB
JavaScript
import {
__name
} from "./chunk-SHUYVCID.js";
// src/polling.ts
var SmartPoller = class {
constructor(baseInterval, task, onError) {
this.baseInterval = baseInterval;
this.task = task;
this.onError = onError;
this.timer = null;
this.consecutiveErrors = 0;
this.maxRetries = 5;
this.currentInterval = baseInterval;
this.maxInterval = Math.min(baseInterval * 10, 3e5);
}
static {
__name(this, "SmartPoller");
}
start() {
this.stop();
this.scheduleNext();
}
stop() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
scheduleNext() {
const jitter = Math.random() * this.currentInterval * 0.25;
const delay = this.currentInterval + jitter;
this.timer = setTimeout(() => {
this.execute();
}, delay);
}
async execute() {
try {
await this.task();
this.consecutiveErrors = 0;
this.currentInterval = this.baseInterval;
} catch (error) {
this.consecutiveErrors++;
if (this.consecutiveErrors <= this.maxRetries) {
this.currentInterval = Math.min(
this.baseInterval * Math.pow(2, this.consecutiveErrors),
this.maxInterval
);
}
this.onError?.(error);
}
this.scheduleNext();
}
/**
* Force an immediate refresh (e.g., on reconnection)
*/
async refreshNow() {
this.stop();
await this.execute();
}
};
// src/context-sanitizer.ts
var DEFAULT_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
// User attributes
"userId",
"organizationId",
"teamId",
"role",
"plan",
"subscription",
// Device/environment
"device",
"browser",
"os",
"platform",
"version",
"locale",
"timezone",
// Application state
"page",
"route",
"feature",
"experiment",
// Safe business attributes
"country",
"region",
"environment",
"buildVersion"
]);
var FORBIDDEN_FIELDS = /* @__PURE__ */ new Set([
"password",
"token",
"apiKey",
"secret",
"creditCard",
"ssn",
"socialSecurityNumber",
"driverLicense",
"passport",
"bankAccount",
"privateKey",
"sessionToken",
"refreshToken",
"accessToken",
"authToken"
]);
var SENSITIVE_PATTERNS = [
/password/i,
/secret/i,
/token/i,
/key/i,
/credit/i,
/ssn/i,
/bank/i,
/private/i,
/auth/i
];
var ContextSanitizer = class {
static {
__name(this, "ContextSanitizer");
}
constructor(options = {}) {
this.allowedFields = options.allowedFields || DEFAULT_ALLOWED_FIELDS;
this.maxSizeForUrl = options.maxSizeForUrl || 2048;
this.maxSizeForBody = options.maxSizeForBody || 10240;
this.strict = options.strict ?? true;
this.warnOnDrop = options.warnOnDrop ?? process.env.NODE_ENV === "development";
}
/**
* Sanitizes context for URL parameters (GET requests).
* More strict size limits due to URL length constraints.
*/
sanitizeForUrl(context) {
const sanitized = this.sanitize(context);
if (Object.keys(sanitized).length === 0) {
return void 0;
}
const serialized = JSON.stringify(sanitized);
if (serialized.length > this.maxSizeForUrl) {
const essential = this.extractEssentialFields(sanitized);
const essentialSerialized = JSON.stringify(essential);
if (essentialSerialized.length > this.maxSizeForUrl) {
if (this.warnOnDrop) {
console.warn(
`[feature-flags] Context too large for URL (${serialized.length} bytes). Maximum allowed: ${this.maxSizeForUrl} bytes. Consider using fewer fields.`
);
}
return void 0;
}
return essentialSerialized;
}
return serialized;
}
/**
* Sanitizes context for request body (POST requests).
* Less strict size limits than URL.
*/
sanitizeForBody(context) {
const sanitized = this.sanitize(context);
if (Object.keys(sanitized).length === 0) {
return void 0;
}
const serialized = JSON.stringify(sanitized);
if (serialized.length > this.maxSizeForBody) {
if (this.warnOnDrop) {
console.warn(
`[feature-flags] Context too large for request (${serialized.length} bytes). Maximum allowed: ${this.maxSizeForBody} bytes. Some fields will be dropped.`
);
}
return this.reduceToSize(sanitized, this.maxSizeForBody);
}
return sanitized;
}
/**
* Core sanitization logic.
*/
sanitize(context) {
const result = {};
const droppedFields = [];
for (const [key, value] of Object.entries(context)) {
if (FORBIDDEN_FIELDS.has(key)) {
droppedFields.push(`${key} (forbidden)`);
continue;
}
if (SENSITIVE_PATTERNS.some((pattern) => pattern.test(key))) {
droppedFields.push(`${key} (sensitive pattern)`);
continue;
}
if (this.strict && !this.allowedFields.has(key)) {
droppedFields.push(`${key} (not in allowlist)`);
continue;
}
if (value === null || value === void 0) {
result[key] = value;
} else if (typeof value === "object" && !Array.isArray(value)) {
const sanitizedNested = this.sanitize(value);
if (Object.keys(sanitizedNested).length > 0) {
result[key] = sanitizedNested;
}
} else if (Array.isArray(value)) {
result[key] = value.slice(0, 10);
} else if (typeof value === "string") {
result[key] = value.length > 200 ? value.substring(0, 200) + "..." : value;
} else if (typeof value !== "function" && typeof value !== "symbol") {
result[key] = value;
}
}
if (this.warnOnDrop && droppedFields.length > 0) {
console.warn(
`[feature-flags] Dropped context fields for security: ${droppedFields.join(", ")}`
);
}
return result;
}
/**
* Extracts only the most essential fields for minimal context.
*/
extractEssentialFields(context) {
const essentialKeys = [
"userId",
"organizationId",
"role",
"plan",
"device",
"environment"
];
const result = {};
for (const key of essentialKeys) {
if (key in context) {
result[key] = context[key];
}
}
return result;
}
/**
* Progressively removes fields until size is acceptable.
*/
reduceToSize(context, maxSize) {
const entries = Object.entries(context);
let result = { ...context };
entries.sort((a, b) => a[0].length - b[0].length);
for (let i = entries.length - 1; i >= 0; i--) {
const serialized = JSON.stringify(result);
if (serialized.length <= maxSize) {
break;
}
delete result[entries[i][0]];
}
return result;
}
/**
* Validates that context doesn't contain sensitive data.
* Returns array of warnings if issues found.
*/
static validate(context) {
const warnings = [];
const checkObject = /* @__PURE__ */ __name((obj, path = "") => {
for (const [key, value] of Object.entries(obj)) {
const fullPath = path ? `${path}.${key}` : key;
if (FORBIDDEN_FIELDS.has(key)) {
warnings.push(
`Forbidden field "${fullPath}" detected - will be removed`
);
}
if (SENSITIVE_PATTERNS.some((pattern) => pattern.test(key))) {
warnings.push(
`Potentially sensitive field "${fullPath}" detected - will be removed`
);
}
if (value && typeof value === "object") {
checkObject(value, fullPath);
}
}
}, "checkObject");
checkObject(context);
return warnings;
}
};
var defaultSanitizer = new ContextSanitizer();
// src/override-manager.ts
var SecureOverrideManager = class {
constructor(config = {}) {
this.overrides = /* @__PURE__ */ new Map();
this.ttl = config.ttl ?? 36e5;
this.allowInProduction = config.allowInProduction ?? false;
this.persist = config.persist ?? false;
this.storageKey = `${config.keyPrefix ?? "feature-flags"}-overrides`;
this.environment = config.environment ?? this.detectEnvironment();
if (this.persist && this.isOverrideAllowed()) {
this.loadFromStorage();
}
this.startCleanupTimer();
}
static {
__name(this, "SecureOverrideManager");
}
/**
* Detect if we're in a production environment.
*
* @returns true if production, false otherwise
* @decision Check multiple indicators for robustness:
* - NODE_ENV (most common)
* - window.location.hostname (production domains)
* - Build-time flags (VITE_ENV, etc.)
*/
detectEnvironment() {
if (typeof process !== "undefined" && process.env?.NODE_ENV) {
return process.env.NODE_ENV;
}
if (typeof window !== "undefined") {
const hostname = window.location.hostname;
if (hostname !== "localhost" && !hostname.startsWith("127.") && !hostname.startsWith("192.168.") && !hostname.includes(".local") && !hostname.includes("staging")) {
return "production";
}
if (typeof import.meta?.env?.MODE !== "undefined") {
return import.meta.env.MODE;
}
}
return "development";
}
/**
* Check if overrides are allowed in current environment.
*/
isOverrideAllowed() {
if (this.environment === "production" && !this.allowInProduction) {
return false;
}
return true;
}
/**
* Set a feature flag override.
*
* @security Blocked in production unless explicitly allowed.
* @param flag - Flag key
* @param value - Override value
* @returns true if override was set, false if blocked
*/
set(flag, value) {
if (!this.isOverrideAllowed()) {
console.warn(
"[feature-flags] Overrides are disabled in production. Set allowInProduction: true to enable (not recommended)."
);
return false;
}
const override = {
value,
expires: Date.now() + this.ttl,
environment: this.environment
};
this.overrides.set(flag, override);
if (this.persist) {
this.saveToStorage();
}
if (this.environment === "development") {
console.debug(
`[feature-flags] Override set: ${flag} = ${JSON.stringify(value)}, expires in ${this.ttl}ms`
);
}
return true;
}
/**
* Get an override value if it exists and hasn't expired.
*/
get(flag) {
if (!this.isOverrideAllowed()) {
return void 0;
}
const override = this.overrides.get(flag);
if (!override) {
return void 0;
}
if (Date.now() > override.expires) {
this.overrides.delete(flag);
if (this.persist) {
this.saveToStorage();
}
return void 0;
}
if (override.environment !== this.environment) {
console.warn(
`[feature-flags] Override "${flag}" was set in ${override.environment} but current is ${this.environment}`
);
}
return override.value;
}
/**
* Check if an override exists (without returning value).
*/
has(flag) {
return this.get(flag) !== void 0;
}
/**
* Clear a specific override.
*/
delete(flag) {
this.overrides.delete(flag);
if (this.persist) {
this.saveToStorage();
}
}
/**
* Clear all overrides.
*/
clear() {
this.overrides.clear();
if (this.persist) {
this.clearStorage();
}
}
/**
* Get all active overrides (for debugging).
*/
getAll() {
if (!this.isOverrideAllowed()) {
return {};
}
const result = {};
for (const [key, override] of this.overrides) {
if (Date.now() <= override.expires) {
result[key] = override.value;
}
}
return result;
}
/**
* Load overrides from localStorage.
*/
loadFromStorage() {
if (typeof globalThis.localStorage === "undefined") {
return;
}
try {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
return;
}
const data = JSON.parse(stored);
const now = Date.now();
for (const [key, override] of Object.entries(data)) {
if (override.expires > now) {
this.overrides.set(key, override);
}
}
} catch (error) {
console.warn(
"[feature-flags] Failed to load overrides from storage:",
error
);
}
}
/**
* Save overrides to localStorage.
*/
saveToStorage() {
if (typeof globalThis.localStorage === "undefined") {
return;
}
try {
const data = {};
for (const [key, override] of this.overrides) {
data[key] = override;
}
localStorage.setItem(this.storageKey, JSON.stringify(data));
} catch (error) {
console.warn(
"[feature-flags] Failed to save overrides to storage:",
error
);
}
}
/**
* Clear overrides from localStorage.
*/
clearStorage() {
if (typeof globalThis.localStorage === "undefined") {
return;
}
try {
localStorage.removeItem(this.storageKey);
} catch (error) {
console.warn(
"[feature-flags] Failed to clear overrides from storage:",
error
);
}
}
/**
* Start periodic cleanup of expired overrides.
*/
startCleanupTimer() {
this.cleanupTimer = setInterval(() => {
this.cleanupExpired();
}, 6e4);
if (typeof process !== "undefined" && this.cleanupTimer.unref) {
this.cleanupTimer.unref();
}
}
/**
* Remove expired overrides.
*/
cleanupExpired() {
const now = Date.now();
let hasChanges = false;
for (const [key, override] of this.overrides) {
if (override.expires <= now) {
this.overrides.delete(key);
hasChanges = true;
}
}
if (hasChanges && this.persist) {
this.saveToStorage();
}
}
/**
* Clean up resources.
*/
dispose() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
}
};
// src/client.ts
var FlagCache = class {
// Tracks LRU order
constructor(options = {}) {
this.cache = /* @__PURE__ */ new Map();
this.storage = null;
this.accessOrder = [];
this.keyPrefix = options.keyPrefix || "ff_";
this.version = options.version || "1";
this.defaultTTL = options.ttl || 6e4;
this.maxEntries = 100;
this.include = options.include;
this.exclude = options.exclude;
this.keyPrefix = `${this.keyPrefix}${this.version}_`;
if (typeof globalThis !== "undefined" && options.storage !== "memory") {
try {
const storage = options.storage === "sessionStorage" ? globalThis.sessionStorage : globalThis.localStorage;
if (storage) {
const testKey = `${this.keyPrefix}_test`;
storage.setItem(testKey, "test");
storage.removeItem(testKey);
this.storage = storage;
this.loadFromStorage();
this.cleanOldVersions();
}
} catch {
this.storage = null;
}
}
}
static {
__name(this, "FlagCache");
}
shouldCache(key) {
if (this.exclude?.includes(key)) return false;
if (this.include && !this.include.includes(key)) return false;
return true;
}
cleanOldVersions() {
if (!this.storage) return;
const keys = Object.keys(this.storage);
for (const key of keys) {
if (key.startsWith("ff_") && !key.startsWith(this.keyPrefix)) {
this.storage.removeItem(key);
}
}
}
loadFromStorage() {
if (!this.storage) return;
const keys = Object.keys(this.storage);
for (const key of keys) {
if (key.startsWith(this.keyPrefix)) {
try {
const data = JSON.parse(this.storage.getItem(key) || "");
if (data && typeof data === "object" && "value" in data) {
const flagKey = key.slice(this.keyPrefix.length);
if (!data.sessionId || data.sessionId === this.currentSessionId) {
this.cache.set(flagKey, data);
this.accessOrder.push(flagKey);
}
}
} catch {
this.storage.removeItem(key);
}
}
}
}
evictLRU() {
if (this.cache.size >= this.maxEntries && this.accessOrder.length > 0) {
const lruKey = this.accessOrder.shift();
if (lruKey) {
this.cache.delete(lruKey);
if (this.storage) {
this.storage.removeItem(`${this.keyPrefix}${lruKey}`);
}
}
}
}
updateAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.accessOrder.push(key);
}
safeStorageWrite(key, value) {
if (!this.storage) return false;
try {
this.storage.setItem(key, value);
return true;
} catch (e) {
if (e?.name === "QuotaExceededError" || e?.code === 22) {
this.clearOldestStorageEntries(5);
try {
this.storage.setItem(key, value);
return true;
} catch {
console.warn(
"[feature-flags] Storage quota exceeded, using memory only"
);
return false;
}
}
return false;
}
}
clearOldestStorageEntries(count) {
if (!this.storage) return;
const toRemove = this.accessOrder.slice(0, count);
for (const key of toRemove) {
this.storage.removeItem(`${this.keyPrefix}${key}`);
}
}
get(key) {
if (!this.shouldCache(key)) return void 0;
const entry = this.cache.get(key);
if (!entry) return void 0;
if (Date.now() - entry.timestamp > entry.ttl) {
this.delete(key);
return void 0;
}
if (entry.sessionId && entry.sessionId !== this.currentSessionId) {
this.delete(key);
return void 0;
}
this.updateAccessOrder(key);
return entry.value;
}
set(key, value, ttl) {
if (!this.shouldCache(key)) return;
this.evictLRU();
const entry = {
value,
timestamp: Date.now(),
ttl: ttl || this.defaultTTL,
sessionId: this.currentSessionId
};
this.cache.set(key, entry);
this.updateAccessOrder(key);
if (this.storage) {
const storageKey = `${this.keyPrefix}${key}`;
this.safeStorageWrite(storageKey, JSON.stringify(entry));
}
}
delete(key) {
this.cache.delete(key);
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
if (this.storage) {
this.storage.removeItem(`${this.keyPrefix}${key}`);
}
}
clear() {
this.cache.clear();
this.accessOrder = [];
if (this.storage) {
const keys = Object.keys(this.storage);
for (const key of keys) {
if (key.startsWith(this.keyPrefix)) {
this.storage.removeItem(key);
}
}
}
}
// Batch get reduces N cache lookups to 1 pass.
getMany(keys) {
const results = /* @__PURE__ */ new Map();
for (const key of keys) {
const value = this.get(key);
if (value !== void 0) {
results.set(key, value);
}
}
return results;
}
setMany(entries, ttl) {
for (const [key, value] of Object.entries(entries)) {
this.set(key, value, ttl);
}
}
/**
* Session change triggers full cache clear.
* Prevents flag leakage between users on shared devices.
*/
invalidateOnSessionChange(newSessionId) {
const sessionChanged = newSessionId !== this.currentSessionId;
this.currentSessionId = newSessionId;
if (sessionChanged) {
this.clear();
}
}
// Get current session for tracking
getCurrentSessionId() {
return this.currentSessionId;
}
};
function featureFlagsClient(options = {}) {
const cache = new FlagCache(options.cache);
const overrideManager = new SecureOverrideManager(options.overrides);
const subscribers = /* @__PURE__ */ new Set();
let context = {};
let cachedFlags = {};
let smartPoller = null;
let sessionUnsubscribe = null;
let lastSessionId = void 0;
const sanitizer = new ContextSanitizer({
strict: options.contextSanitization?.strict ?? true,
allowedFields: options.contextSanitization?.allowedFields ? new Set(options.contextSanitization.allowedFields) : void 0,
maxSizeForUrl: options.contextSanitization?.maxUrlSize,
maxSizeForBody: options.contextSanitization?.maxBodySize,
warnOnDrop: options.contextSanitization?.warnOnDrop
});
const sanitizationEnabled = options.contextSanitization?.enabled ?? true;
const notifySubscribers = /* @__PURE__ */ __name((flags) => {
cachedFlags = flags;
subscribers.forEach((callback) => callback(flags));
}, "notifySubscribers");
return {
id: "feature-flags",
$InferServerPlugin: {},
getAtoms: /* @__PURE__ */ __name((atoms) => {
if (atoms?.session) {
const unsubscribe = atoms.session.subscribe((sessionState) => {
const currentSessionId = sessionState?.data?.session?.id;
if (currentSessionId !== lastSessionId) {
lastSessionId = currentSessionId;
cache.invalidateOnSessionChange(currentSessionId);
cachedFlags = {};
if (currentSessionId) {
notifySubscribers({});
}
}
});
sessionUnsubscribe = unsubscribe;
}
return {};
}, "getAtoms"),
getActions: /* @__PURE__ */ __name((fetch) => {
const handleError = /* @__PURE__ */ __name((error) => {
if (options.debug) {
console.error("[feature-flags]", error);
}
options.onError?.(error);
}, "handleError");
const logEvaluation = /* @__PURE__ */ __name((flag, result) => {
if (options.debug) {
console.log(`[feature-flags] ${flag}:`, result);
}
options.onEvaluation?.(flag, result);
}, "logEvaluation");
const evaluateFlag = /* @__PURE__ */ __name(async (key) => {
const overrideValue = overrideManager.get(String(key));
if (overrideValue !== void 0) {
return {
value: overrideValue,
reason: "override"
};
}
const cached = cache.get(String(key));
if (cached !== void 0) {
logEvaluation(String(key), cached);
return cached;
}
try {
const params = new URLSearchParams();
const keyStr = String(key);
if (options.defaults?.[key] !== void 0) {
params.set(
"default",
JSON.stringify(options.defaults[key])
);
}
if (Object.keys(context).length > 0) {
const sanitizedContext = sanitizationEnabled ? sanitizer.sanitizeForUrl(context) : JSON.stringify(context);
if (sanitizedContext) {
params.set("context", sanitizedContext);
}
}
const response = await fetch(
`/api/flags/evaluate/${keyStr}?${params}`,
{
method: "GET"
}
);
const result = response.data;
cache.set(keyStr, result);
logEvaluation(keyStr, result);
return result;
} catch (error) {
handleError(error);
if (options.defaults?.[key] !== void 0) {
return {
value: options.defaults[key],
reason: "default"
};
}
return {
value: void 0,
reason: "not_found"
};
}
}, "evaluateFlag");
const actions = {
featureFlags: {
async isEnabled(flag, defaultValue = false) {
const result = await evaluateFlag(flag);
const value = result.value ?? defaultValue;
return Boolean(value);
},
async getValue(flag, defaultValue) {
const result = await evaluateFlag(flag);
return result.value ?? defaultValue;
},
async getVariant(flag) {
const result = await evaluateFlag(flag);
return result.variant || null;
},
async getAllFlags() {
try {
const params = new URLSearchParams();
if (Object.keys(context).length > 0) {
const sanitizedContext = sanitizationEnabled ? sanitizer.sanitizeForUrl(context) : JSON.stringify(context);
if (sanitizedContext) {
params.set("context", sanitizedContext);
}
}
const response = await fetch(`/api/flags/all?${params}`, {
method: "GET"
});
const data = response.data;
const flags = {};
for (const [key, result] of Object.entries(data.flags)) {
flags[key] = result.value;
cache.set(key, result);
}
notifySubscribers(flags);
return flags;
} catch (error) {
handleError(error);
return options.defaults || {};
}
},
async evaluateBatch(keys) {
const cachedResults = cache.getMany(keys.map(String));
const results = {};
const uncachedKeys = [];
for (const key of keys) {
const cached = cachedResults.get(String(key));
if (cached) {
results[key] = cached;
logEvaluation(String(key), cached);
} else {
uncachedKeys.push(String(key));
}
}
if (uncachedKeys.length === 0) {
return results;
}
try {
const response = await fetch("/api/flags/evaluate/batch", {
method: "POST",
body: {
keys: uncachedKeys,
defaults: options.defaults ? Object.fromEntries(
uncachedKeys.filter(
(k) => options.defaults[k] !== void 0
).map((k) => [
k,
options.defaults[k]
])
) : void 0,
context: Object.keys(context).length > 0 ? sanitizationEnabled ? sanitizer.sanitizeForBody(context) : context : void 0
}
});
const data = response.data;
cache.setMany(data.flags);
for (const [key, result] of Object.entries(data.flags)) {
results[key] = result;
logEvaluation(key, result);
}
return results;
} catch (error) {
handleError(error);
for (const key of uncachedKeys) {
results[key] = {
value: options.defaults?.[key],
reason: "default"
};
}
return results;
}
},
async track(flag, event, value) {
try {
await fetch("/api/flags/track", {
method: "POST",
body: {
flagKey: String(flag),
event,
data: value
}
});
} catch (error) {
handleError(error);
}
},
setContext(newContext) {
if (options.debug && sanitizationEnabled) {
const warnings = ContextSanitizer.validate(newContext);
if (warnings.length > 0) {
console.warn(
"[feature-flags] Context validation warnings:\n" + warnings.join("\n")
);
}
}
context = { ...context, ...newContext };
cache.clear();
},
getContext() {
return { ...context };
},
async prefetch(flags) {
const uncached = flags.filter(
(key) => cache.get(String(key)) === void 0
);
if (uncached.length > 0) {
await actions.featureFlags.evaluateBatch(uncached);
}
},
clearCache() {
cache.clear();
},
setOverride(flag, value) {
const success = overrideManager.set(String(flag), value);
if (success) {
notifySubscribers({ ...cachedFlags, [flag]: value });
}
},
clearOverrides() {
overrideManager.clear();
actions.featureFlags.refresh();
},
async refresh() {
cache.clear();
const flags = await actions.featureFlags.getAllFlags();
notifySubscribers(flags);
},
subscribe(callback) {
subscribers.add(callback);
callback(cachedFlags);
return () => {
subscribers.delete(callback);
};
},
dispose() {
if (smartPoller) {
smartPoller.stop();
smartPoller = null;
}
if (sessionUnsubscribe) {
sessionUnsubscribe();
sessionUnsubscribe = null;
}
cache.clear();
subscribers.clear();
overrideManager.dispose();
}
}
};
if (options.polling?.enabled && options.polling.interval && typeof globalThis !== "undefined" && typeof globalThis.setTimeout === "function") {
if (smartPoller) {
smartPoller.stop();
}
smartPoller = new SmartPoller(
options.polling.interval,
async () => {
await actions.featureFlags.refresh();
},
(error) => {
handleError(new Error(`Polling refresh failed: ${error.message}`));
}
);
smartPoller.start();
}
return actions;
}, "getActions")
};
}
__name(featureFlagsClient, "featureFlagsClient");
var client_default = featureFlagsClient;
export {
client_default as default,
featureFlagsClient
};