@mergedapp/feature-flags 0.1.0
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 +435 -0
- package/dist/cli.js +578 -0
- package/dist/index.cjs +897 -0
- package/dist/index.d.cts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +856 -0
- package/dist/react.cjs +239 -0
- package/dist/react.d.cts +223 -0
- package/dist/react.d.ts +223 -0
- package/dist/react.js +213 -0
- package/package.json +68 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var src_exports = {};
|
|
33
|
+
__export(src_exports, {
|
|
34
|
+
FeatureFlagError: () => FeatureFlagError,
|
|
35
|
+
FeatureFlagNetworkError: () => FeatureFlagNetworkError,
|
|
36
|
+
FeatureFlagVerificationError: () => FeatureFlagVerificationError,
|
|
37
|
+
MergedFeatureFlags: () => MergedFeatureFlags,
|
|
38
|
+
createDefaultFeatureFlagRuntimeStatus: () => createDefaultFeatureFlagRuntimeStatus,
|
|
39
|
+
createFileFeatureFlagSnapshotStore: () => createFileFeatureFlagSnapshotStore,
|
|
40
|
+
createLocalStorageFeatureFlagSnapshotStore: () => createLocalStorageFeatureFlagSnapshotStore
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(src_exports);
|
|
43
|
+
|
|
44
|
+
// src/errors.ts
|
|
45
|
+
var FeatureFlagError = class extends Error {
|
|
46
|
+
static {
|
|
47
|
+
__name(this, "FeatureFlagError");
|
|
48
|
+
}
|
|
49
|
+
constructor(message, options) {
|
|
50
|
+
super(message, options);
|
|
51
|
+
this.name = "FeatureFlagError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var FeatureFlagNetworkError = class extends FeatureFlagError {
|
|
55
|
+
static {
|
|
56
|
+
__name(this, "FeatureFlagNetworkError");
|
|
57
|
+
}
|
|
58
|
+
constructor(message, options) {
|
|
59
|
+
super(message, options);
|
|
60
|
+
this.name = "FeatureFlagNetworkError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var FeatureFlagVerificationError = class extends FeatureFlagError {
|
|
64
|
+
static {
|
|
65
|
+
__name(this, "FeatureFlagVerificationError");
|
|
66
|
+
}
|
|
67
|
+
constructor(message, options) {
|
|
68
|
+
super(message, options);
|
|
69
|
+
this.name = "FeatureFlagVerificationError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/persistence.ts
|
|
74
|
+
var DEFAULT_SNAPSHOT_KEY_PREFIX = "merged-feature-flags";
|
|
75
|
+
var SNAPSHOT_SCHEMA_VERSION = 1;
|
|
76
|
+
var textEncoder = new TextEncoder();
|
|
77
|
+
var textDecoder = new TextDecoder();
|
|
78
|
+
var nodeModulePromise = null;
|
|
79
|
+
async function getNodeModules() {
|
|
80
|
+
if (!nodeModulePromise) {
|
|
81
|
+
nodeModulePromise = Promise.all([
|
|
82
|
+
import("crypto"),
|
|
83
|
+
import("fs/promises"),
|
|
84
|
+
import("os"),
|
|
85
|
+
import("path")
|
|
86
|
+
]).then(([crypto, fs, os, path]) => ({
|
|
87
|
+
crypto,
|
|
88
|
+
fs,
|
|
89
|
+
os,
|
|
90
|
+
path
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
return nodeModulePromise;
|
|
94
|
+
}
|
|
95
|
+
__name(getNodeModules, "getNodeModules");
|
|
96
|
+
function normalizeValue(value) {
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
return value.map((item) => normalizeValue(item));
|
|
99
|
+
}
|
|
100
|
+
if (value && typeof value === "object") {
|
|
101
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [
|
|
102
|
+
key,
|
|
103
|
+
normalizeValue(entryValue)
|
|
104
|
+
]);
|
|
105
|
+
return Object.fromEntries(entries);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
__name(normalizeValue, "normalizeValue");
|
|
110
|
+
function serializeCanonicalValue(value) {
|
|
111
|
+
return JSON.stringify(normalizeValue(value));
|
|
112
|
+
}
|
|
113
|
+
__name(serializeCanonicalValue, "serializeCanonicalValue");
|
|
114
|
+
function createDefaultFeatureFlagRuntimeStatus() {
|
|
115
|
+
return {
|
|
116
|
+
source: "defaults",
|
|
117
|
+
isStale: false,
|
|
118
|
+
lastSuccessfulRefreshAt: null,
|
|
119
|
+
tokenExpiresAt: null,
|
|
120
|
+
lastError: null
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
__name(createDefaultFeatureFlagRuntimeStatus, "createDefaultFeatureFlagRuntimeStatus");
|
|
124
|
+
async function sha256Hex(value) {
|
|
125
|
+
if (globalThis.crypto?.subtle) {
|
|
126
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", textEncoder.encode(value));
|
|
127
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
128
|
+
}
|
|
129
|
+
const { crypto } = await getNodeModules();
|
|
130
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
131
|
+
}
|
|
132
|
+
__name(sha256Hex, "sha256Hex");
|
|
133
|
+
async function buildSnapshotScopeParts(params) {
|
|
134
|
+
const contextHash = await sha256Hex(serializeCanonicalValue(params.config.evaluationContext ?? null));
|
|
135
|
+
const clientKeyFingerprint = await sha256Hex(params.config.clientKey);
|
|
136
|
+
const prefix = params.keyPrefix?.trim() || DEFAULT_SNAPSHOT_KEY_PREFIX;
|
|
137
|
+
const scopeMaterial = serializeCanonicalValue({
|
|
138
|
+
apiUrl: params.config.apiUrl.replace(/\/$/, ""),
|
|
139
|
+
organizationId: params.config.organizationId.trim(),
|
|
140
|
+
environmentId: params.config.environmentId.trim(),
|
|
141
|
+
teamId: params.config.teamId?.trim() || null,
|
|
142
|
+
clientKeyFingerprint,
|
|
143
|
+
contextHash
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
scopeKey: `${prefix}:${await sha256Hex(scopeMaterial)}`,
|
|
147
|
+
organizationId: params.config.organizationId.trim(),
|
|
148
|
+
environmentId: params.config.environmentId.trim(),
|
|
149
|
+
teamId: params.config.teamId?.trim() || null,
|
|
150
|
+
clientKeyFingerprint,
|
|
151
|
+
contextHash
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
__name(buildSnapshotScopeParts, "buildSnapshotScopeParts");
|
|
155
|
+
function buildSnapshotRecord(params) {
|
|
156
|
+
return {
|
|
157
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
158
|
+
scopeKey: params.scope.scopeKey,
|
|
159
|
+
organizationId: params.scope.organizationId,
|
|
160
|
+
environmentId: params.scope.environmentId,
|
|
161
|
+
teamId: params.scope.teamId,
|
|
162
|
+
clientKeyFingerprint: params.scope.clientKeyFingerprint,
|
|
163
|
+
contextHash: params.scope.contextHash,
|
|
164
|
+
token: params.token,
|
|
165
|
+
publicKeyPem: params.publicKeyPem,
|
|
166
|
+
fetchedAt: params.fetchedAt,
|
|
167
|
+
tokenExpiresAt: params.tokenExpiresAt
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
__name(buildSnapshotRecord, "buildSnapshotRecord");
|
|
171
|
+
function createPersistedFeatureFlagSnapshot(params) {
|
|
172
|
+
return buildSnapshotRecord(params);
|
|
173
|
+
}
|
|
174
|
+
__name(createPersistedFeatureFlagSnapshot, "createPersistedFeatureFlagSnapshot");
|
|
175
|
+
function doesSnapshotMatchScope(params) {
|
|
176
|
+
return params.snapshot.schemaVersion === SNAPSHOT_SCHEMA_VERSION && params.snapshot.scopeKey === params.scope.scopeKey && params.snapshot.organizationId === params.scope.organizationId && params.snapshot.environmentId === params.scope.environmentId && params.snapshot.teamId === params.scope.teamId && params.snapshot.clientKeyFingerprint === params.scope.clientKeyFingerprint && params.snapshot.contextHash === params.scope.contextHash;
|
|
177
|
+
}
|
|
178
|
+
__name(doesSnapshotMatchScope, "doesSnapshotMatchScope");
|
|
179
|
+
function isBrowserPersistenceAvailable() {
|
|
180
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
181
|
+
}
|
|
182
|
+
__name(isBrowserPersistenceAvailable, "isBrowserPersistenceAvailable");
|
|
183
|
+
function createLocalStorageFeatureFlagSnapshotStore() {
|
|
184
|
+
return {
|
|
185
|
+
async load(key) {
|
|
186
|
+
if (!isBrowserPersistenceAvailable()) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const raw = window.localStorage.getItem(key);
|
|
191
|
+
return raw ? JSON.parse(raw) : null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
async save(key, snapshot) {
|
|
197
|
+
if (!isBrowserPersistenceAvailable()) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
window.localStorage.setItem(key, JSON.stringify(snapshot));
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
async remove(key) {
|
|
206
|
+
if (!isBrowserPersistenceAvailable()) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
window.localStorage.removeItem(key);
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
__name(createLocalStorageFeatureFlagSnapshotStore, "createLocalStorageFeatureFlagSnapshotStore");
|
|
217
|
+
function sanitizeKeyForFilename(key) {
|
|
218
|
+
return encodeURIComponent(key);
|
|
219
|
+
}
|
|
220
|
+
__name(sanitizeKeyForFilename, "sanitizeKeyForFilename");
|
|
221
|
+
function createFileFeatureFlagSnapshotStore(params) {
|
|
222
|
+
return {
|
|
223
|
+
async load(key) {
|
|
224
|
+
const { fs, os, path } = await getNodeModules();
|
|
225
|
+
const rootDirectory = params?.rootDirectory ?? path.join(os.tmpdir(), DEFAULT_SNAPSHOT_KEY_PREFIX);
|
|
226
|
+
const filePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.json`);
|
|
227
|
+
try {
|
|
228
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
229
|
+
return JSON.parse(raw);
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
async save(key, snapshot) {
|
|
235
|
+
const { fs, os, path } = await getNodeModules();
|
|
236
|
+
const rootDirectory = params?.rootDirectory ?? path.join(os.tmpdir(), DEFAULT_SNAPSHOT_KEY_PREFIX);
|
|
237
|
+
const filePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.json`);
|
|
238
|
+
const tempFilePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
239
|
+
await fs.mkdir(rootDirectory, {
|
|
240
|
+
recursive: true
|
|
241
|
+
});
|
|
242
|
+
await fs.writeFile(tempFilePath, JSON.stringify(snapshot), "utf-8");
|
|
243
|
+
await fs.rename(tempFilePath, filePath);
|
|
244
|
+
},
|
|
245
|
+
async remove(key) {
|
|
246
|
+
const { fs, os, path } = await getNodeModules();
|
|
247
|
+
const rootDirectory = params?.rootDirectory ?? path.join(os.tmpdir(), DEFAULT_SNAPSHOT_KEY_PREFIX);
|
|
248
|
+
const filePath = path.join(rootDirectory, `${sanitizeKeyForFilename(key)}.json`);
|
|
249
|
+
try {
|
|
250
|
+
await fs.rm(filePath, {
|
|
251
|
+
force: true
|
|
252
|
+
});
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
__name(createFileFeatureFlagSnapshotStore, "createFileFeatureFlagSnapshotStore");
|
|
259
|
+
async function resolveSnapshotStore(params) {
|
|
260
|
+
const persistence = params.config.snapshotPersistence;
|
|
261
|
+
if (persistence === false) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (persistence?.store) {
|
|
265
|
+
return persistence.store;
|
|
266
|
+
}
|
|
267
|
+
if (isBrowserPersistenceAvailable()) {
|
|
268
|
+
return createLocalStorageFeatureFlagSnapshotStore();
|
|
269
|
+
}
|
|
270
|
+
return createFileFeatureFlagSnapshotStore();
|
|
271
|
+
}
|
|
272
|
+
__name(resolveSnapshotStore, "resolveSnapshotStore");
|
|
273
|
+
function getSnapshotKeyPrefix(params) {
|
|
274
|
+
const persistence = params.config.snapshotPersistence;
|
|
275
|
+
return persistence?.keyPrefix?.trim() || DEFAULT_SNAPSHOT_KEY_PREFIX;
|
|
276
|
+
}
|
|
277
|
+
__name(getSnapshotKeyPrefix, "getSnapshotKeyPrefix");
|
|
278
|
+
|
|
279
|
+
// src/jwt.ts
|
|
280
|
+
var import_jose = require("jose");
|
|
281
|
+
var JWT_ALGORITHM = "ES256";
|
|
282
|
+
var JWT_ISSUER = "merged";
|
|
283
|
+
var JWT_TYPE = "feature_flag_values";
|
|
284
|
+
var cachedKey = null;
|
|
285
|
+
var cachedPem = null;
|
|
286
|
+
async function importPublicKey(pem) {
|
|
287
|
+
if (cachedPem === pem && cachedKey) {
|
|
288
|
+
return cachedKey;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
cachedKey = await (0, import_jose.importSPKI)(pem, JWT_ALGORITHM);
|
|
292
|
+
cachedPem = pem;
|
|
293
|
+
return cachedKey;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
cachedKey = null;
|
|
296
|
+
cachedPem = null;
|
|
297
|
+
throw new FeatureFlagVerificationError("Failed to import public key.", {
|
|
298
|
+
cause: error
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
__name(importPublicKey, "importPublicKey");
|
|
303
|
+
function resetPublicKeyCache() {
|
|
304
|
+
cachedKey = null;
|
|
305
|
+
cachedPem = null;
|
|
306
|
+
}
|
|
307
|
+
__name(resetPublicKeyCache, "resetPublicKeyCache");
|
|
308
|
+
async function verifyFeatureFlagTokenDetailed(params) {
|
|
309
|
+
try {
|
|
310
|
+
if (!params.allowExpired) {
|
|
311
|
+
const { payload: payload2 } = await (0, import_jose.jwtVerify)(params.token, params.publicKey, {
|
|
312
|
+
algorithms: [
|
|
313
|
+
JWT_ALGORITHM
|
|
314
|
+
],
|
|
315
|
+
issuer: JWT_ISSUER
|
|
316
|
+
});
|
|
317
|
+
return extractVerifiedTokenPayload(payload2);
|
|
318
|
+
}
|
|
319
|
+
const { payload } = await (0, import_jose.compactVerify)(params.token, params.publicKey, {
|
|
320
|
+
algorithms: [
|
|
321
|
+
JWT_ALGORITHM
|
|
322
|
+
]
|
|
323
|
+
});
|
|
324
|
+
const rawPayload = JSON.parse(new TextDecoder().decode(payload));
|
|
325
|
+
const notBefore = rawPayload.nbf;
|
|
326
|
+
if (typeof notBefore === "number" && Number.isFinite(notBefore) && Math.floor(Date.now() / 1e3) < notBefore) {
|
|
327
|
+
throw new FeatureFlagVerificationError("JWT payload is not yet valid.");
|
|
328
|
+
}
|
|
329
|
+
return extractVerifiedTokenPayload(rawPayload);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error instanceof FeatureFlagVerificationError) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
throw new FeatureFlagVerificationError("JWT verification failed.", {
|
|
335
|
+
cause: error
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
__name(verifyFeatureFlagTokenDetailed, "verifyFeatureFlagTokenDetailed");
|
|
340
|
+
function extractVerifiedTokenPayload(payload) {
|
|
341
|
+
if (payload.iss !== JWT_ISSUER) {
|
|
342
|
+
throw new FeatureFlagVerificationError("JWT payload has an unexpected issuer.");
|
|
343
|
+
}
|
|
344
|
+
if (payload.type !== JWT_TYPE) {
|
|
345
|
+
throw new FeatureFlagVerificationError("JWT payload has an unexpected token type.");
|
|
346
|
+
}
|
|
347
|
+
const flags = payload.flags;
|
|
348
|
+
if (!Array.isArray(flags)) {
|
|
349
|
+
throw new FeatureFlagVerificationError("JWT payload does not contain a flags array.");
|
|
350
|
+
}
|
|
351
|
+
const expiresAt = typeof payload.exp === "number" && Number.isFinite(payload.exp) ? new Date(payload.exp * 1e3).toISOString() : null;
|
|
352
|
+
return {
|
|
353
|
+
flags,
|
|
354
|
+
expiresAt
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
__name(extractVerifiedTokenPayload, "extractVerifiedTokenPayload");
|
|
358
|
+
|
|
359
|
+
// src/client.ts
|
|
360
|
+
var DEFAULT_REFRESH_INTERVAL_MS = 6e4;
|
|
361
|
+
var BACKOFF_BASE_MS = 5e3;
|
|
362
|
+
var BACKOFF_CAP_MS = 3e5;
|
|
363
|
+
var MergedFeatureFlags = class {
|
|
364
|
+
static {
|
|
365
|
+
__name(this, "MergedFeatureFlags");
|
|
366
|
+
}
|
|
367
|
+
flagsById = /* @__PURE__ */ new Map();
|
|
368
|
+
flagsByName = /* @__PURE__ */ new Map();
|
|
369
|
+
publicKeyPem;
|
|
370
|
+
publicKeyCrypto = null;
|
|
371
|
+
refreshTimer = null;
|
|
372
|
+
backoffMs = 0;
|
|
373
|
+
consecutiveFailures = 0;
|
|
374
|
+
listeners = /* @__PURE__ */ new Set();
|
|
375
|
+
storeListeners = /* @__PURE__ */ new Set();
|
|
376
|
+
initialized = false;
|
|
377
|
+
visibilityHandler = null;
|
|
378
|
+
pendingScopeTransition = false;
|
|
379
|
+
runtimeStatus = createDefaultFeatureFlagRuntimeStatus();
|
|
380
|
+
snapshotStorePromise = null;
|
|
381
|
+
keyPrefix;
|
|
382
|
+
contextSignature;
|
|
383
|
+
flagIds;
|
|
384
|
+
clientKey;
|
|
385
|
+
apiUrl;
|
|
386
|
+
organizationId;
|
|
387
|
+
environmentId;
|
|
388
|
+
teamId;
|
|
389
|
+
refreshInterval;
|
|
390
|
+
onError;
|
|
391
|
+
onFlagsChanged;
|
|
392
|
+
snapshotPersistence;
|
|
393
|
+
evaluationContext;
|
|
394
|
+
constructor(config) {
|
|
395
|
+
this.flagIds = config.flagIds ?? null;
|
|
396
|
+
this.clientKey = config.clientKey;
|
|
397
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
398
|
+
this.organizationId = config.organizationId.trim();
|
|
399
|
+
this.environmentId = config.environmentId.trim();
|
|
400
|
+
this.teamId = config.teamId?.trim() || null;
|
|
401
|
+
this.publicKeyPem = config.publicKey ?? null;
|
|
402
|
+
this.refreshInterval = config.refreshInterval ?? DEFAULT_REFRESH_INTERVAL_MS;
|
|
403
|
+
this.onError = config.onError ?? null;
|
|
404
|
+
this.onFlagsChanged = config.onFlagsChanged ?? null;
|
|
405
|
+
this.snapshotPersistence = config.snapshotPersistence;
|
|
406
|
+
this.evaluationContext = config.evaluationContext ?? null;
|
|
407
|
+
this.contextSignature = serializeCanonicalValue(this.evaluationContext ?? null);
|
|
408
|
+
this.keyPrefix = getSnapshotKeyPrefix({
|
|
409
|
+
config
|
|
410
|
+
});
|
|
411
|
+
if (!this.organizationId) {
|
|
412
|
+
throw new TypeError("organizationId is required.");
|
|
413
|
+
}
|
|
414
|
+
if (!this.environmentId) {
|
|
415
|
+
throw new TypeError("environmentId is required.");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async initialize() {
|
|
419
|
+
if (this.initialized) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.initialized = true;
|
|
423
|
+
try {
|
|
424
|
+
const restored = await this.restorePersistedSnapshot().catch(() => false);
|
|
425
|
+
try {
|
|
426
|
+
await this.refresh();
|
|
427
|
+
} catch {
|
|
428
|
+
if (!restored) {
|
|
429
|
+
this.clearFlags();
|
|
430
|
+
this.setRuntimeStatus({
|
|
431
|
+
source: "defaults",
|
|
432
|
+
tokenExpiresAt: null
|
|
433
|
+
});
|
|
434
|
+
this.emitChange({
|
|
435
|
+
flagsChanged: true,
|
|
436
|
+
storeChanged: true
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} finally {
|
|
441
|
+
if (this.refreshInterval > 0) {
|
|
442
|
+
this.startPolling();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/** Returns true if the flag is enabled. Returns false for unknown flags. */
|
|
447
|
+
isEnabled(name) {
|
|
448
|
+
return this.resolveFlag(name)?.enabled ?? false;
|
|
449
|
+
}
|
|
450
|
+
/** Returns the flag's value with the type inferred from the flag registry. */
|
|
451
|
+
getValue(name) {
|
|
452
|
+
const flag = this.resolveFlag(name);
|
|
453
|
+
return flag?.value;
|
|
454
|
+
}
|
|
455
|
+
/** Returns the full evaluated flag entry, or undefined if not found. */
|
|
456
|
+
getFlag(name) {
|
|
457
|
+
return this.resolveFlag(name);
|
|
458
|
+
}
|
|
459
|
+
/** Returns all evaluated flags. */
|
|
460
|
+
getAllFlags() {
|
|
461
|
+
return Array.from(this.flagsById.values());
|
|
462
|
+
}
|
|
463
|
+
getStatus() {
|
|
464
|
+
const tokenExpiresAt = this.runtimeStatus.tokenExpiresAt;
|
|
465
|
+
const isStale = tokenExpiresAt != null && Number.isFinite(new Date(tokenExpiresAt).getTime()) && new Date(tokenExpiresAt).getTime() <= Date.now();
|
|
466
|
+
return {
|
|
467
|
+
...this.runtimeStatus,
|
|
468
|
+
isStale
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
getStatusSnapshot() {
|
|
472
|
+
return this.getStatus();
|
|
473
|
+
}
|
|
474
|
+
/** Manually trigger a refresh from the server. */
|
|
475
|
+
async refresh() {
|
|
476
|
+
const restoredForCurrentScope = this.pendingScopeTransition ? await this.restorePersistedSnapshot().catch(() => false) : false;
|
|
477
|
+
try {
|
|
478
|
+
await this.ensureVerificationKey();
|
|
479
|
+
const response = await this.fetchSignedFlags();
|
|
480
|
+
const verifiedFlags = await this.verifyWithRetry(response.token);
|
|
481
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
482
|
+
const flagsChanged = this.updateFlags(verifiedFlags.flags);
|
|
483
|
+
this.resetBackoff();
|
|
484
|
+
this.pendingScopeTransition = false;
|
|
485
|
+
this.setRuntimeStatus({
|
|
486
|
+
source: "network",
|
|
487
|
+
tokenExpiresAt: verifiedFlags.expiresAt,
|
|
488
|
+
lastSuccessfulRefreshAt: fetchedAt,
|
|
489
|
+
lastError: null
|
|
490
|
+
});
|
|
491
|
+
await this.persistSnapshot({
|
|
492
|
+
fetchedAt,
|
|
493
|
+
token: response.token,
|
|
494
|
+
tokenExpiresAt: verifiedFlags.expiresAt
|
|
495
|
+
});
|
|
496
|
+
this.emitChange({
|
|
497
|
+
flagsChanged,
|
|
498
|
+
storeChanged: true
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
this.incrementBackoff();
|
|
502
|
+
const wrappedError = error instanceof Error ? error : new FeatureFlagNetworkError("Unknown error during refresh.");
|
|
503
|
+
if (this.pendingScopeTransition && !restoredForCurrentScope) {
|
|
504
|
+
const hadFlags = this.flagsById.size > 0;
|
|
505
|
+
this.clearFlags();
|
|
506
|
+
this.pendingScopeTransition = false;
|
|
507
|
+
this.setRuntimeStatus({
|
|
508
|
+
source: "defaults",
|
|
509
|
+
tokenExpiresAt: null,
|
|
510
|
+
lastSuccessfulRefreshAt: null,
|
|
511
|
+
lastError: wrappedError
|
|
512
|
+
});
|
|
513
|
+
this.emitChange({
|
|
514
|
+
flagsChanged: hadFlags,
|
|
515
|
+
storeChanged: true
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
this.setRuntimeStatus({
|
|
519
|
+
lastError: wrappedError
|
|
520
|
+
});
|
|
521
|
+
this.emitChange({
|
|
522
|
+
flagsChanged: false,
|
|
523
|
+
storeChanged: true
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
this.onError?.(wrappedError);
|
|
527
|
+
throw wrappedError;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** Subscribe to flag changes. Returns an unsubscribe function. */
|
|
531
|
+
onChange(listener) {
|
|
532
|
+
this.listeners.add(listener);
|
|
533
|
+
return () => {
|
|
534
|
+
this.listeners.delete(listener);
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/** Get a snapshot function for useSyncExternalStore. */
|
|
538
|
+
getSnapshot() {
|
|
539
|
+
return this.getAllFlags();
|
|
540
|
+
}
|
|
541
|
+
/** Replace the caller-provided evaluation context used on subsequent refreshes. */
|
|
542
|
+
setEvaluationContext(context) {
|
|
543
|
+
const nextContext = context ?? null;
|
|
544
|
+
const nextSignature = serializeCanonicalValue(nextContext ?? null);
|
|
545
|
+
if (nextSignature === this.contextSignature) {
|
|
546
|
+
this.evaluationContext = nextContext;
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
this.evaluationContext = nextContext;
|
|
550
|
+
this.contextSignature = nextSignature;
|
|
551
|
+
this.pendingScopeTransition = true;
|
|
552
|
+
}
|
|
553
|
+
/** Subscribe function for useSyncExternalStore. */
|
|
554
|
+
subscribe(onStoreChange) {
|
|
555
|
+
this.storeListeners.add(onStoreChange);
|
|
556
|
+
return () => {
|
|
557
|
+
this.storeListeners.delete(onStoreChange);
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/** Clean up timers, listeners, and cached data. */
|
|
561
|
+
destroy() {
|
|
562
|
+
if (this.refreshTimer) {
|
|
563
|
+
clearTimeout(this.refreshTimer);
|
|
564
|
+
this.refreshTimer = null;
|
|
565
|
+
}
|
|
566
|
+
if (this.visibilityHandler && typeof document !== "undefined") {
|
|
567
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
568
|
+
this.visibilityHandler = null;
|
|
569
|
+
}
|
|
570
|
+
this.listeners.clear();
|
|
571
|
+
this.storeListeners.clear();
|
|
572
|
+
this.clearFlags();
|
|
573
|
+
this.pendingScopeTransition = false;
|
|
574
|
+
this.runtimeStatus = createDefaultFeatureFlagRuntimeStatus();
|
|
575
|
+
this.initialized = false;
|
|
576
|
+
}
|
|
577
|
+
resolveFlag(name) {
|
|
578
|
+
if (this.flagIds && name in this.flagIds) {
|
|
579
|
+
const id = this.flagIds[name];
|
|
580
|
+
return this.flagsById.get(id);
|
|
581
|
+
}
|
|
582
|
+
return this.flagsById.get(name) ?? this.flagsByName.get(name);
|
|
583
|
+
}
|
|
584
|
+
clearFlags() {
|
|
585
|
+
this.flagsById.clear();
|
|
586
|
+
this.flagsByName.clear();
|
|
587
|
+
}
|
|
588
|
+
emitChange(params) {
|
|
589
|
+
const allFlags = this.getAllFlags();
|
|
590
|
+
if (params.flagsChanged) {
|
|
591
|
+
this.onFlagsChanged?.(allFlags);
|
|
592
|
+
for (const listener of this.listeners) {
|
|
593
|
+
listener(allFlags);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (params.storeChanged) {
|
|
597
|
+
for (const listener of this.storeListeners) {
|
|
598
|
+
listener();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
updateFlags(flags) {
|
|
603
|
+
const oldFlags = new Map(this.flagsById);
|
|
604
|
+
this.clearFlags();
|
|
605
|
+
for (const flag of flags) {
|
|
606
|
+
this.flagsById.set(flag.id, flag);
|
|
607
|
+
this.flagsByName.set(flag.name, flag);
|
|
608
|
+
}
|
|
609
|
+
return this.hasFlagsChanged(oldFlags, this.flagsById);
|
|
610
|
+
}
|
|
611
|
+
hasFlagsChanged(oldFlags, newFlags) {
|
|
612
|
+
if (oldFlags.size !== newFlags.size) {
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
for (const [id, newFlag] of newFlags) {
|
|
616
|
+
const oldFlag = oldFlags.get(id);
|
|
617
|
+
if (!oldFlag) {
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
if (oldFlag.enabled !== newFlag.enabled || oldFlag.name !== newFlag.name || JSON.stringify(oldFlag.value) !== JSON.stringify(newFlag.value)) {
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
setRuntimeStatus(patch) {
|
|
627
|
+
this.runtimeStatus = {
|
|
628
|
+
...this.runtimeStatus,
|
|
629
|
+
...patch
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async fetchSignedFlags() {
|
|
633
|
+
const url = new URL("/api/feature-flags/evaluate/signed", this.apiUrl);
|
|
634
|
+
const payload = {
|
|
635
|
+
organizationId: this.organizationId,
|
|
636
|
+
environmentId: this.environmentId,
|
|
637
|
+
...this.teamId ? {
|
|
638
|
+
teamId: this.teamId
|
|
639
|
+
} : {},
|
|
640
|
+
...this.evaluationContext ? {
|
|
641
|
+
context: this.evaluationContext
|
|
642
|
+
} : {}
|
|
643
|
+
};
|
|
644
|
+
let response;
|
|
645
|
+
try {
|
|
646
|
+
response = await fetch(url, {
|
|
647
|
+
method: "POST",
|
|
648
|
+
headers: {
|
|
649
|
+
Authorization: `Bearer ${this.clientKey}`,
|
|
650
|
+
"Content-Type": "application/json",
|
|
651
|
+
Accept: "application/json"
|
|
652
|
+
},
|
|
653
|
+
body: JSON.stringify(payload)
|
|
654
|
+
});
|
|
655
|
+
} catch (error) {
|
|
656
|
+
throw new FeatureFlagNetworkError("Failed to fetch feature flags.", {
|
|
657
|
+
cause: error
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
throw new FeatureFlagNetworkError(`Feature flag API returned ${response.status}: ${response.statusText}`);
|
|
662
|
+
}
|
|
663
|
+
return await response.json();
|
|
664
|
+
}
|
|
665
|
+
async fetchPublicKey() {
|
|
666
|
+
const url = `${this.apiUrl}/api/feature-flags/public-key`;
|
|
667
|
+
let response;
|
|
668
|
+
try {
|
|
669
|
+
response = await fetch(url);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
throw new FeatureFlagNetworkError("Failed to fetch public key.", {
|
|
672
|
+
cause: error
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (!response.ok) {
|
|
676
|
+
throw new FeatureFlagNetworkError(`Public key endpoint returned ${response.status}: ${response.statusText}`);
|
|
677
|
+
}
|
|
678
|
+
const body = await response.json();
|
|
679
|
+
return body.publicKey;
|
|
680
|
+
}
|
|
681
|
+
async verifyWithRetry(token) {
|
|
682
|
+
try {
|
|
683
|
+
return await verifyFeatureFlagTokenDetailed({
|
|
684
|
+
token,
|
|
685
|
+
publicKey: this.publicKeyCrypto
|
|
686
|
+
});
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (!(error instanceof FeatureFlagVerificationError)) {
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
resetPublicKeyCache();
|
|
692
|
+
this.publicKeyPem = await this.fetchPublicKey();
|
|
693
|
+
this.publicKeyCrypto = await importPublicKey(this.publicKeyPem);
|
|
694
|
+
return verifyFeatureFlagTokenDetailed({
|
|
695
|
+
token,
|
|
696
|
+
publicKey: this.publicKeyCrypto
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async ensureVerificationKey() {
|
|
701
|
+
if (!this.publicKeyPem) {
|
|
702
|
+
this.publicKeyPem = await this.fetchPublicKey();
|
|
703
|
+
}
|
|
704
|
+
if (!this.publicKeyCrypto) {
|
|
705
|
+
this.publicKeyCrypto = await importPublicKey(this.publicKeyPem);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async getTrustedSnapshotVerificationKey() {
|
|
709
|
+
if (this.publicKeyPem) {
|
|
710
|
+
if (!this.publicKeyCrypto) {
|
|
711
|
+
this.publicKeyCrypto = await importPublicKey(this.publicKeyPem);
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
publicKeyCrypto: this.publicKeyCrypto,
|
|
715
|
+
publicKeyPem: this.publicKeyPem
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
await this.ensureVerificationKey();
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (error instanceof FeatureFlagNetworkError) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
if (!this.publicKeyPem || !this.publicKeyCrypto) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
publicKeyCrypto: this.publicKeyCrypto,
|
|
731
|
+
publicKeyPem: this.publicKeyPem
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async getSnapshotStore() {
|
|
735
|
+
if (!this.snapshotStorePromise) {
|
|
736
|
+
let snapshotPersistence;
|
|
737
|
+
if (this.snapshotPersistence === void 0) {
|
|
738
|
+
snapshotPersistence = {
|
|
739
|
+
keyPrefix: this.keyPrefix
|
|
740
|
+
};
|
|
741
|
+
} else if (this.snapshotPersistence === false) {
|
|
742
|
+
snapshotPersistence = false;
|
|
743
|
+
} else {
|
|
744
|
+
snapshotPersistence = {
|
|
745
|
+
...this.snapshotPersistence,
|
|
746
|
+
keyPrefix: this.snapshotPersistence.keyPrefix ?? this.keyPrefix
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
this.snapshotStorePromise = resolveSnapshotStore({
|
|
750
|
+
config: {
|
|
751
|
+
apiUrl: this.apiUrl,
|
|
752
|
+
clientKey: this.clientKey,
|
|
753
|
+
environmentId: this.environmentId,
|
|
754
|
+
organizationId: this.organizationId,
|
|
755
|
+
teamId: this.teamId ?? void 0,
|
|
756
|
+
evaluationContext: this.evaluationContext ?? void 0,
|
|
757
|
+
snapshotPersistence
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return this.snapshotStorePromise;
|
|
762
|
+
}
|
|
763
|
+
async getScopeParts() {
|
|
764
|
+
return buildSnapshotScopeParts({
|
|
765
|
+
config: {
|
|
766
|
+
apiUrl: this.apiUrl,
|
|
767
|
+
clientKey: this.clientKey,
|
|
768
|
+
environmentId: this.environmentId,
|
|
769
|
+
organizationId: this.organizationId,
|
|
770
|
+
teamId: this.teamId ?? void 0,
|
|
771
|
+
evaluationContext: this.evaluationContext ?? void 0
|
|
772
|
+
},
|
|
773
|
+
keyPrefix: this.keyPrefix
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
async restorePersistedSnapshot() {
|
|
777
|
+
const store = await this.getSnapshotStore();
|
|
778
|
+
if (!store) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
const scope = await this.getScopeParts();
|
|
782
|
+
const snapshot = await store.load(scope.scopeKey);
|
|
783
|
+
if (!snapshot) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
if (!doesSnapshotMatchScope({
|
|
787
|
+
snapshot,
|
|
788
|
+
scope
|
|
789
|
+
})) {
|
|
790
|
+
await store.remove(scope.scopeKey);
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
const trustedKey = await this.getTrustedSnapshotVerificationKey();
|
|
794
|
+
if (!trustedKey) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const verified = await verifyFeatureFlagTokenDetailed({
|
|
799
|
+
token: snapshot.token,
|
|
800
|
+
publicKey: trustedKey.publicKeyCrypto,
|
|
801
|
+
allowExpired: true
|
|
802
|
+
});
|
|
803
|
+
this.publicKeyPem = trustedKey.publicKeyPem;
|
|
804
|
+
this.publicKeyCrypto = trustedKey.publicKeyCrypto;
|
|
805
|
+
this.pendingScopeTransition = false;
|
|
806
|
+
const flagsChanged = this.updateFlags(verified.flags);
|
|
807
|
+
this.setRuntimeStatus({
|
|
808
|
+
source: "persisted",
|
|
809
|
+
tokenExpiresAt: verified.expiresAt,
|
|
810
|
+
lastSuccessfulRefreshAt: snapshot.fetchedAt,
|
|
811
|
+
lastError: null
|
|
812
|
+
});
|
|
813
|
+
this.emitChange({
|
|
814
|
+
flagsChanged,
|
|
815
|
+
storeChanged: true
|
|
816
|
+
});
|
|
817
|
+
return true;
|
|
818
|
+
} catch {
|
|
819
|
+
await store.remove(scope.scopeKey);
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async persistSnapshot(params) {
|
|
824
|
+
const store = await this.getSnapshotStore();
|
|
825
|
+
if (!store || !this.publicKeyPem) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const scope = await this.getScopeParts();
|
|
829
|
+
const snapshot = createPersistedFeatureFlagSnapshot({
|
|
830
|
+
scope,
|
|
831
|
+
token: params.token,
|
|
832
|
+
publicKeyPem: this.publicKeyPem,
|
|
833
|
+
fetchedAt: params.fetchedAt,
|
|
834
|
+
tokenExpiresAt: params.tokenExpiresAt
|
|
835
|
+
});
|
|
836
|
+
try {
|
|
837
|
+
await store.save(scope.scopeKey, snapshot);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
const wrappedError = error instanceof Error ? error : new Error("Failed to persist feature flag snapshot.");
|
|
840
|
+
this.onError?.(wrappedError);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
startPolling() {
|
|
844
|
+
this.scheduleNextPoll();
|
|
845
|
+
if (typeof document !== "undefined") {
|
|
846
|
+
this.visibilityHandler = () => {
|
|
847
|
+
if (document.visibilityState === "hidden") {
|
|
848
|
+
if (this.refreshTimer) {
|
|
849
|
+
clearTimeout(this.refreshTimer);
|
|
850
|
+
this.refreshTimer = null;
|
|
851
|
+
}
|
|
852
|
+
} else if (document.visibilityState === "visible") {
|
|
853
|
+
void this.safeRefresh();
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
scheduleNextPoll() {
|
|
860
|
+
if (this.refreshTimer) {
|
|
861
|
+
clearTimeout(this.refreshTimer);
|
|
862
|
+
}
|
|
863
|
+
const delay = this.backoffMs > 0 ? this.backoffMs : this.refreshInterval;
|
|
864
|
+
this.refreshTimer = setTimeout(() => {
|
|
865
|
+
void this.safeRefresh();
|
|
866
|
+
}, delay);
|
|
867
|
+
}
|
|
868
|
+
async safeRefresh() {
|
|
869
|
+
try {
|
|
870
|
+
await this.refresh();
|
|
871
|
+
} catch {
|
|
872
|
+
} finally {
|
|
873
|
+
const isPageVisible = typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
874
|
+
if (this.initialized && this.refreshInterval > 0 && isPageVisible) {
|
|
875
|
+
this.scheduleNextPoll();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
resetBackoff() {
|
|
880
|
+
this.backoffMs = 0;
|
|
881
|
+
this.consecutiveFailures = 0;
|
|
882
|
+
}
|
|
883
|
+
incrementBackoff() {
|
|
884
|
+
this.consecutiveFailures++;
|
|
885
|
+
this.backoffMs = Math.min(BACKOFF_BASE_MS * 2 ** (this.consecutiveFailures - 1), BACKOFF_CAP_MS);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
889
|
+
0 && (module.exports = {
|
|
890
|
+
FeatureFlagError,
|
|
891
|
+
FeatureFlagNetworkError,
|
|
892
|
+
FeatureFlagVerificationError,
|
|
893
|
+
MergedFeatureFlags,
|
|
894
|
+
createDefaultFeatureFlagRuntimeStatus,
|
|
895
|
+
createFileFeatureFlagSnapshotStore,
|
|
896
|
+
createLocalStorageFeatureFlagSnapshotStore
|
|
897
|
+
});
|