@guidelytech/flags 0.9.0 → 0.11.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/bin/guidely-flags.js +3 -0
- package/dist/{chunk-2RZKAT37.js → chunk-Y7T74UZV.js} +2 -2
- package/dist/chunk-Y7T74UZV.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-2RZKAT37.js.map +0 -1
- package/dist/chunk-K2HOYVSV.js +0 -1085
- package/dist/chunk-K2HOYVSV.js.map +0 -1
- package/dist/chunk-ZTY7K7P2.js +0 -988
- package/dist/chunk-ZTY7K7P2.js.map +0 -1
package/dist/chunk-ZTY7K7P2.js
DELETED
|
@@ -1,988 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__publicField,
|
|
3
|
-
decodeUtf8,
|
|
4
|
-
evaluateFlag,
|
|
5
|
-
textOf
|
|
6
|
-
} from "./chunk-OY4QS3WL.js";
|
|
7
|
-
|
|
8
|
-
// src/util.ts
|
|
9
|
-
function stableStringify(value) {
|
|
10
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
11
|
-
if (Array.isArray(value)) return `[${value.map((v) => v === void 0 ? "null" : stableStringify(v)).join(",")}]`;
|
|
12
|
-
if (value instanceof Date) return JSON.stringify(value.toISOString());
|
|
13
|
-
const obj = value;
|
|
14
|
-
return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
15
|
-
}
|
|
16
|
-
function isEncryptedPayload(x) {
|
|
17
|
-
return !!x && typeof x === "object" && typeof x.encryptedPayload === "string";
|
|
18
|
-
}
|
|
19
|
-
function isRemoteResults(x) {
|
|
20
|
-
return !!x && typeof x === "object" && !("schemaVersion" in x) && !!x.flags && typeof x.flags === "object";
|
|
21
|
-
}
|
|
22
|
-
function isFlagsPayload(x) {
|
|
23
|
-
return !!x && typeof x === "object" && "schemaVersion" in x && !!x.flags && typeof x.flags === "object";
|
|
24
|
-
}
|
|
25
|
-
function kindOf(v) {
|
|
26
|
-
if (v === null || v === void 0) return "null";
|
|
27
|
-
if (Array.isArray(v)) return "array";
|
|
28
|
-
return typeof v;
|
|
29
|
-
}
|
|
30
|
-
function compact(attrs) {
|
|
31
|
-
const out = {};
|
|
32
|
-
for (const [k, v] of Object.entries(attrs)) if (v !== void 0) out[k] = v;
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
35
|
-
function parseUrlOverrides(search, param = "guidely_flags") {
|
|
36
|
-
const out = {};
|
|
37
|
-
let raw = null;
|
|
38
|
-
try {
|
|
39
|
-
raw = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search).get(param);
|
|
40
|
-
} catch {
|
|
41
|
-
return out;
|
|
42
|
-
}
|
|
43
|
-
if (!raw) return out;
|
|
44
|
-
const items = [];
|
|
45
|
-
let depth = 0;
|
|
46
|
-
let quoted = false;
|
|
47
|
-
let start = 0;
|
|
48
|
-
for (let i = 0; i < raw.length; i++) {
|
|
49
|
-
const c = raw[i];
|
|
50
|
-
if (quoted) {
|
|
51
|
-
if (c === "\\") i++;
|
|
52
|
-
else if (c === '"') quoted = false;
|
|
53
|
-
} else if (c === '"') quoted = true;
|
|
54
|
-
else if (c === "[" || c === "{") depth++;
|
|
55
|
-
else if (c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
56
|
-
else if (c === "," && depth === 0) {
|
|
57
|
-
items.push(raw.slice(start, i));
|
|
58
|
-
start = i + 1;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
items.push(raw.slice(start));
|
|
62
|
-
for (const item of items) {
|
|
63
|
-
const sep = item.indexOf(":");
|
|
64
|
-
if (sep <= 0) continue;
|
|
65
|
-
const key = item.slice(0, sep).trim();
|
|
66
|
-
const text = item.slice(sep + 1).trim();
|
|
67
|
-
if (!key) continue;
|
|
68
|
-
try {
|
|
69
|
-
out[key] = JSON.parse(text);
|
|
70
|
-
} catch {
|
|
71
|
-
out[key] = text;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
function isDev() {
|
|
77
|
-
try {
|
|
78
|
-
const p = globalThis.process;
|
|
79
|
-
return !!p?.env && p.env.NODE_ENV !== "production";
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function timer(fn, ms) {
|
|
85
|
-
const t = setTimeout(fn, ms);
|
|
86
|
-
t.unref?.();
|
|
87
|
-
return t;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// src/evaluator.ts
|
|
91
|
-
function typeMatches(value, fallback) {
|
|
92
|
-
if (value === null || value === void 0) return false;
|
|
93
|
-
if (fallback === null || fallback === void 0) return true;
|
|
94
|
-
return kindOf(value) === kindOf(fallback);
|
|
95
|
-
}
|
|
96
|
-
function makeEvaluator(resolve, all, attributes) {
|
|
97
|
-
const details = (key, fallback) => {
|
|
98
|
-
const r = resolve(key);
|
|
99
|
-
const ok = typeMatches(r.value, fallback);
|
|
100
|
-
return { ...r, value: ok ? r.value : fallback, usedFallback: !ok };
|
|
101
|
-
};
|
|
102
|
-
const of = (type, key, fallback) => {
|
|
103
|
-
const v = resolve(key).value;
|
|
104
|
-
return kindOf(v) === type ? v : fallback;
|
|
105
|
-
};
|
|
106
|
-
return {
|
|
107
|
-
isEnabled: (key, fallback = false) => of("boolean", key, fallback),
|
|
108
|
-
getValue: (key, fallback) => details(key, fallback).value,
|
|
109
|
-
getString: (key, fallback) => of("string", key, fallback),
|
|
110
|
-
getNumber: (key, fallback) => of("number", key, fallback),
|
|
111
|
-
getBoolean: (key, fallback) => of("boolean", key, fallback),
|
|
112
|
-
getJson: (key, fallback) => {
|
|
113
|
-
const v = resolve(key).value;
|
|
114
|
-
return v !== null && typeof v === "object" ? v : fallback;
|
|
115
|
-
},
|
|
116
|
-
getDetails: details,
|
|
117
|
-
getAll: all,
|
|
118
|
-
getAttributes: attributes
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function notFound(key) {
|
|
122
|
-
return { key, value: null, reason: "FLAG_NOT_FOUND", inExperiment: false };
|
|
123
|
-
}
|
|
124
|
-
function createStaticFlags(input = {}) {
|
|
125
|
-
const attrs = { ...input.attributes ?? {} };
|
|
126
|
-
const p = input.payload;
|
|
127
|
-
const payload = isFlagsPayload(p) && !isEncryptedPayload(p) ? p : null;
|
|
128
|
-
const results = !payload && isRemoteResults(p) ? p.flags : null;
|
|
129
|
-
const resolve = (key) => {
|
|
130
|
-
try {
|
|
131
|
-
if (payload) return evaluateFlag(payload, key, attrs, { now: input.now });
|
|
132
|
-
return results?.[key] ?? notFound(key);
|
|
133
|
-
} catch {
|
|
134
|
-
return notFound(key);
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
const all = () => {
|
|
138
|
-
const out = {};
|
|
139
|
-
for (const key of Object.keys(payload?.flags ?? results ?? {})) out[key] = resolve(key).value;
|
|
140
|
-
return out;
|
|
141
|
-
};
|
|
142
|
-
return { ...makeEvaluator(resolve, all, () => ({ ...attrs })), isReady: () => !!(payload || results), subscribe: () => () => {
|
|
143
|
-
} };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// src/crypto.ts
|
|
147
|
-
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
148
|
-
function base64ToBytes(input) {
|
|
149
|
-
const s = input.trim().replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
|
|
150
|
-
const out = new Uint8Array(Math.floor(s.length * 3 / 4));
|
|
151
|
-
let buffer = 0;
|
|
152
|
-
let bits = 0;
|
|
153
|
-
let n = 0;
|
|
154
|
-
for (let i = 0; i < s.length; i++) {
|
|
155
|
-
const v = ALPHABET.indexOf(s[i]);
|
|
156
|
-
if (v < 0) throw new Error("invalid base64");
|
|
157
|
-
buffer = buffer << 6 | v;
|
|
158
|
-
bits += 6;
|
|
159
|
-
if (bits >= 8) {
|
|
160
|
-
bits -= 8;
|
|
161
|
-
out[n++] = buffer >> bits & 255;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return out.subarray(0, n);
|
|
165
|
-
}
|
|
166
|
-
async function decryptPayloadString(encrypted, decryptionKey) {
|
|
167
|
-
const subtle = globalThis.crypto?.subtle;
|
|
168
|
-
if (!subtle) throw new Error("WebCrypto (crypto.subtle) is not available in this runtime; encrypted payloads need it (Node \u2265 19, browsers, edge runtimes, or a polyfill)");
|
|
169
|
-
const [ivText, dataText] = encrypted.split(".");
|
|
170
|
-
if (!ivText || !dataText) throw new Error("malformed encrypted payload");
|
|
171
|
-
const key = base64ToBytes(decryptionKey);
|
|
172
|
-
if (key.length !== 32) throw new Error("decryptionKey must be base64 of 32 bytes");
|
|
173
|
-
const cryptoKey = await subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
174
|
-
const plain = await subtle.decrypt({ name: "AES-GCM", iv: base64ToBytes(ivText) }, cryptoKey, base64ToBytes(dataText));
|
|
175
|
-
return decodeUtf8(new Uint8Array(plain));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// src/sse.ts
|
|
179
|
-
function createSseParser(onEvent) {
|
|
180
|
-
let buffer = "";
|
|
181
|
-
const dispatch = (raw) => {
|
|
182
|
-
let event = "message";
|
|
183
|
-
const data = [];
|
|
184
|
-
for (const line of raw.split("\n")) {
|
|
185
|
-
if (!line || line.startsWith(":")) continue;
|
|
186
|
-
const i = line.indexOf(":");
|
|
187
|
-
const field = i < 0 ? line : line.slice(0, i);
|
|
188
|
-
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
189
|
-
if (field === "event") event = value;
|
|
190
|
-
else if (field === "data") data.push(value);
|
|
191
|
-
}
|
|
192
|
-
if (data.length) onEvent(event, data.join("\n"));
|
|
193
|
-
};
|
|
194
|
-
return {
|
|
195
|
-
feed(chunk) {
|
|
196
|
-
buffer += chunk.replace(/\r\n?/g, "\n");
|
|
197
|
-
let sep;
|
|
198
|
-
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
199
|
-
dispatch(buffer.slice(0, sep));
|
|
200
|
-
buffer = buffer.slice(sep + 2);
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
end() {
|
|
204
|
-
if (buffer.trim()) dispatch(buffer);
|
|
205
|
-
buffer = "";
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// src/client.ts
|
|
211
|
-
var FLAGS_SDK_NAME = "@guidelytech/flags";
|
|
212
|
-
var FLAGS_SDK_VERSION = "0.7.0";
|
|
213
|
-
var DEFAULT_FLAGS_API_URL = "https://guidelytech.com";
|
|
214
|
-
var SECRET_PREFIX = "gly_sk_";
|
|
215
|
-
var MAX_RESULT_CACHE = 20;
|
|
216
|
-
var MAX_QUEUE = 1e3;
|
|
217
|
-
var USAGE_FLUSH_MS = 6e4;
|
|
218
|
-
var EXPOSURE_FLUSH_MS = 5e3;
|
|
219
|
-
var MAX_BACKOFF_MS = 5 * 6e4;
|
|
220
|
-
var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
221
|
-
function defaultStorage() {
|
|
222
|
-
try {
|
|
223
|
-
if (!isBrowser() || typeof localStorage === "undefined") return null;
|
|
224
|
-
const probe = "__guidely_flags_probe__";
|
|
225
|
-
localStorage.setItem(probe, "1");
|
|
226
|
-
localStorage.removeItem(probe);
|
|
227
|
-
return localStorage;
|
|
228
|
-
} catch {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
function backoff(failures, capMs) {
|
|
233
|
-
const base = Math.min(capMs, 1e3 * 2 ** Math.max(0, failures - 1));
|
|
234
|
-
return Math.round(base * (0.8 + Math.random() * 0.4));
|
|
235
|
-
}
|
|
236
|
-
var FlagsClient = class {
|
|
237
|
-
constructor(options) {
|
|
238
|
-
__publicField(this, "sdkKey");
|
|
239
|
-
__publicField(this, "apiUrl");
|
|
240
|
-
__publicField(this, "environment");
|
|
241
|
-
__publicField(this, "o");
|
|
242
|
-
__publicField(this, "cfg");
|
|
243
|
-
__publicField(this, "secret");
|
|
244
|
-
__publicField(this, "log");
|
|
245
|
-
__publicField(this, "storage");
|
|
246
|
-
__publicField(this, "storageKey");
|
|
247
|
-
__publicField(this, "getters");
|
|
248
|
-
__publicField(this, "payload", null);
|
|
249
|
-
/** The payload as received (possibly encrypted): what `getPayload()` hands to browsers. */
|
|
250
|
-
__publicField(this, "raw", null);
|
|
251
|
-
__publicField(this, "etag", null);
|
|
252
|
-
__publicField(this, "remote");
|
|
253
|
-
__publicField(this, "results", null);
|
|
254
|
-
__publicField(this, "resultsKey", null);
|
|
255
|
-
__publicField(this, "resultCache", /* @__PURE__ */ new Map());
|
|
256
|
-
__publicField(this, "context");
|
|
257
|
-
__publicField(this, "attributes");
|
|
258
|
-
__publicField(this, "overrides");
|
|
259
|
-
__publicField(this, "urlOverrides");
|
|
260
|
-
__publicField(this, "offline");
|
|
261
|
-
__publicField(this, "destroyed", false);
|
|
262
|
-
__publicField(this, "ready_", false);
|
|
263
|
-
__publicField(this, "resolveReady");
|
|
264
|
-
__publicField(this, "readyPromise");
|
|
265
|
-
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
266
|
-
__publicField(this, "inflight", null);
|
|
267
|
-
__publicField(this, "lastFetchAt", 0);
|
|
268
|
-
__publicField(this, "failures", 0);
|
|
269
|
-
__publicField(this, "retryAt", 0);
|
|
270
|
-
__publicField(this, "pollTimer", null);
|
|
271
|
-
__publicField(this, "remoteTimer", null);
|
|
272
|
-
__publicField(this, "flushTimer", null);
|
|
273
|
-
__publicField(this, "flushDue", Infinity);
|
|
274
|
-
__publicField(this, "stream", null);
|
|
275
|
-
__publicField(this, "streamConnected", false);
|
|
276
|
-
__publicField(this, "streamFailures", 0);
|
|
277
|
-
__publicField(this, "reconnectTimer", null);
|
|
278
|
-
__publicField(this, "exposed", /* @__PURE__ */ new Set());
|
|
279
|
-
__publicField(this, "usage", /* @__PURE__ */ new Map());
|
|
280
|
-
__publicField(this, "exposures", []);
|
|
281
|
-
__publicField(this, "warned", /* @__PURE__ */ new Set());
|
|
282
|
-
__publicField(this, "cleanups", []);
|
|
283
|
-
__publicField(this, "lastError", "");
|
|
284
|
-
/** Calls `listener` whenever values may have changed (payload, attributes, overrides, readiness). */
|
|
285
|
-
__publicField(this, "subscribe", (listener) => {
|
|
286
|
-
this.listeners.add(listener);
|
|
287
|
-
return () => void this.listeners.delete(listener);
|
|
288
|
-
});
|
|
289
|
-
const o = options ?? {};
|
|
290
|
-
this.o = o;
|
|
291
|
-
const key = o.sdkKey ?? o.projectId ?? "";
|
|
292
|
-
if (!key) throw new Error("[Guidely flags] `sdkKey` (your environment public key pk_\u2026, or a gly_sk_\u2026 secret key on servers) is required.");
|
|
293
|
-
this.sdkKey = key;
|
|
294
|
-
this.secret = key.startsWith(SECRET_PREFIX);
|
|
295
|
-
this.environment = o.environment;
|
|
296
|
-
this.apiUrl = (o.apiUrl || DEFAULT_FLAGS_API_URL).replace(/\/+$/, "");
|
|
297
|
-
this.log = o.logger === null ? {} : o.logger ?? { warn: (...a) => console.warn("[Guidely flags]", ...a), error: (...a) => console.error("[Guidely flags]", ...a) };
|
|
298
|
-
if (this.secret && !o.environment) throw new Error("[Guidely flags] `environment` (development | staging | production) is required with a gly_sk_ key.");
|
|
299
|
-
if (this.secret && isBrowser()) this.log.warn?.("A gly_sk_ secret key is being used in a browser. Secret keys belong on servers; use the environment's public pk_ key here.");
|
|
300
|
-
this.cfg = {
|
|
301
|
-
mode: o.pollingMode ?? "auto",
|
|
302
|
-
pollMs: Math.max(1, o.pollIntervalSeconds ?? 60) * 1e3,
|
|
303
|
-
ttlMs: Math.max(0, o.cacheTtlSeconds ?? 60) * 1e3,
|
|
304
|
-
debounceMs: Math.max(0, o.remoteEvalDebounceMs ?? 50),
|
|
305
|
-
behaviour: o.overrideBehaviour ?? "localOverRemote",
|
|
306
|
-
trackExposures: o.trackExposures ?? !o.trackingCallback,
|
|
307
|
-
reportUsage: o.reportUsage ?? true
|
|
308
|
-
};
|
|
309
|
-
this.remote = !!o.remoteEval;
|
|
310
|
-
this.offline = !!o.offline;
|
|
311
|
-
this.context = compact(o.contextAttributes ?? {});
|
|
312
|
-
this.attributes = compact(o.attributes ?? {});
|
|
313
|
-
this.overrides = { ...o.overrides ?? {} };
|
|
314
|
-
this.urlOverrides = o.enableUrlOverrides && isBrowser() ? parseUrlOverrides(location.search) : {};
|
|
315
|
-
this.storage = o.storage === false || o.storage === null ? null : o.storage ?? defaultStorage();
|
|
316
|
-
this.storageKey = `guidely:flags:${key}${o.environment && this.secret ? `:${o.environment}` : ""}`;
|
|
317
|
-
this.readyPromise = new Promise((r) => this.resolveReady = r);
|
|
318
|
-
this.getters = makeEvaluator(
|
|
319
|
-
(k) => this.resolve(k, this.getAttributes(), true),
|
|
320
|
-
() => this.all(this.getAttributes()),
|
|
321
|
-
() => this.getAttributes()
|
|
322
|
-
);
|
|
323
|
-
const boot = o.payload;
|
|
324
|
-
if (isRemoteResults(boot)) this.setResults(boot, stableStringify(this.getAttributes()), false);
|
|
325
|
-
else if (isFlagsPayload(boot) && !isEncryptedPayload(boot)) this.setPayload(boot, boot, null, false);
|
|
326
|
-
if (o.remoteResults) {
|
|
327
|
-
const r = o.remoteResults;
|
|
328
|
-
this.setResults(isRemoteResults(r) && typeof r.version === "number" ? r : { version: 0, flags: o.remoteResults }, stableStringify(this.getAttributes()), false);
|
|
329
|
-
}
|
|
330
|
-
if (this.results) this.remote = true;
|
|
331
|
-
this.wireBrowser();
|
|
332
|
-
if (this.payload || this.results) this.settle();
|
|
333
|
-
void this.start();
|
|
334
|
-
}
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
// Getters (never throw)
|
|
337
|
-
// ---------------------------------------------------------------------------
|
|
338
|
-
/** Boolean flag value; `fallback` (default `false`) when missing or not a boolean. */
|
|
339
|
-
isEnabled(key, fallback = false) {
|
|
340
|
-
return this.getters.isEnabled(key, fallback);
|
|
341
|
-
}
|
|
342
|
-
/** Flag value; `fallback` when missing, null or of a different JSON type than `fallback`. */
|
|
343
|
-
getValue(key, fallback) {
|
|
344
|
-
return this.getters.getValue(key, fallback);
|
|
345
|
-
}
|
|
346
|
-
getString(key, fallback) {
|
|
347
|
-
return this.getters.getString(key, fallback);
|
|
348
|
-
}
|
|
349
|
-
getNumber(key, fallback) {
|
|
350
|
-
return this.getters.getNumber(key, fallback);
|
|
351
|
-
}
|
|
352
|
-
getBoolean(key, fallback) {
|
|
353
|
-
return this.getters.getBoolean(key, fallback);
|
|
354
|
-
}
|
|
355
|
-
getJson(key, fallback) {
|
|
356
|
-
return this.getters.getJson(key, fallback);
|
|
357
|
-
}
|
|
358
|
-
/** Value plus reason, rule, variation and experiment assignment. */
|
|
359
|
-
getDetails(key, fallback) {
|
|
360
|
-
return this.getters.getDetails(key, fallback);
|
|
361
|
-
}
|
|
362
|
-
/** Every flag's value for the current attributes (records no exposures or usage). */
|
|
363
|
-
getAll() {
|
|
364
|
-
return this.getters.getAll();
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Getters bound to other attributes (merged over the client's), without changing the client:
|
|
368
|
-
* the per-request pattern on servers. With remote evaluation they serve results already cached
|
|
369
|
-
* for these attributes, else fallbacks.
|
|
370
|
-
*/
|
|
371
|
-
forUser(attributes) {
|
|
372
|
-
const attrs = { ...this.getAttributes(), ...compact(attributes ?? {}) };
|
|
373
|
-
if (this.remote) this.warnOnce("forUser:remote", "forUser() with remote evaluation only serves results cached for those attributes.");
|
|
374
|
-
return makeEvaluator(
|
|
375
|
-
(k) => this.resolve(k, attrs, true),
|
|
376
|
-
() => this.all(attrs),
|
|
377
|
-
() => ({ ...attrs })
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
// ---------------------------------------------------------------------------
|
|
381
|
-
// Attributes and overrides
|
|
382
|
-
// ---------------------------------------------------------------------------
|
|
383
|
-
/** Replaces the app attributes. */
|
|
384
|
-
setAttributes(attributes) {
|
|
385
|
-
this.changeAttributes(() => this.attributes = compact(attributes ?? {}));
|
|
386
|
-
}
|
|
387
|
-
/** Merges into the app attributes. */
|
|
388
|
-
updateAttributes(partial) {
|
|
389
|
-
this.changeAttributes(() => this.attributes = compact({ ...this.attributes, ...partial }));
|
|
390
|
-
}
|
|
391
|
-
/** Replaces the integration-supplied attributes (see `contextAttributes`). */
|
|
392
|
-
setContextAttributes(attributes) {
|
|
393
|
-
this.changeAttributes(() => this.context = compact(attributes ?? {}));
|
|
394
|
-
}
|
|
395
|
-
/** Context attributes with app attributes merged over them. */
|
|
396
|
-
getAttributes() {
|
|
397
|
-
return { ...this.context, ...this.attributes };
|
|
398
|
-
}
|
|
399
|
-
/** Replaces the overrides (`null` clears them). URL overrides stay. */
|
|
400
|
-
setOverrides(overrides) {
|
|
401
|
-
this.overrides = { ...overrides ?? {} };
|
|
402
|
-
this.notify();
|
|
403
|
-
}
|
|
404
|
-
changeAttributes(apply) {
|
|
405
|
-
const before = stableStringify(this.getAttributes());
|
|
406
|
-
apply();
|
|
407
|
-
const after = stableStringify(this.getAttributes());
|
|
408
|
-
if (before === after) return;
|
|
409
|
-
if (this.remote) {
|
|
410
|
-
const cached = this.resultCache.get(after);
|
|
411
|
-
if (cached) {
|
|
412
|
-
this.results = cached;
|
|
413
|
-
this.resultsKey = after;
|
|
414
|
-
}
|
|
415
|
-
this.scheduleRemote();
|
|
416
|
-
}
|
|
417
|
-
this.notify();
|
|
418
|
-
}
|
|
419
|
-
// ---------------------------------------------------------------------------
|
|
420
|
-
// Lifecycle
|
|
421
|
-
// ---------------------------------------------------------------------------
|
|
422
|
-
/** Resolves once the first load settled (bootstrap, network success or failure). Never rejects. */
|
|
423
|
-
ready() {
|
|
424
|
-
return this.readyPromise;
|
|
425
|
-
}
|
|
426
|
-
isReady() {
|
|
427
|
-
return this.ready_;
|
|
428
|
-
}
|
|
429
|
-
/** `true` stops all network activity; cached data keeps serving. `false` resumes and refreshes. */
|
|
430
|
-
setOffline(offline) {
|
|
431
|
-
if (this.offline === offline) return;
|
|
432
|
-
this.offline = offline;
|
|
433
|
-
if (offline) return this.stopNetwork();
|
|
434
|
-
void this.refresh().then(() => this.schedule());
|
|
435
|
-
}
|
|
436
|
-
/** Fetches now (payload, or remote evaluation). Resolves when done; never rejects. */
|
|
437
|
-
async refresh() {
|
|
438
|
-
await this.fetchNow();
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* The current data for server-side rendering: pass it as `payload` to the browser client (or
|
|
442
|
-
* framework provider) to render the same values without a flash. Encrypted payloads stay
|
|
443
|
-
* encrypted; with remote evaluation it returns the results. `null` before anything loaded.
|
|
444
|
-
*/
|
|
445
|
-
getPayload() {
|
|
446
|
-
if (this.remote) return this.results;
|
|
447
|
-
return this.raw;
|
|
448
|
-
}
|
|
449
|
-
/** Sends pending usage counts and exposures now. */
|
|
450
|
-
async flush() {
|
|
451
|
-
const batch = this.takeBatch();
|
|
452
|
-
if (!batch) return;
|
|
453
|
-
try {
|
|
454
|
-
const res = await this.request("POST", "/flags/events", { body: batch, keepalive: true });
|
|
455
|
-
if (!res.ok && (res.status >= 500 || res.status === 429)) this.requeue(batch);
|
|
456
|
-
} catch {
|
|
457
|
-
this.requeue(batch);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/** Stops timers and streams and sends pending usage. The client keeps serving cached values. */
|
|
461
|
-
destroy() {
|
|
462
|
-
if (this.destroyed) return;
|
|
463
|
-
this.flushOnExit();
|
|
464
|
-
this.destroyed = true;
|
|
465
|
-
this.stopNetwork();
|
|
466
|
-
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
467
|
-
this.cleanups.splice(0).forEach((fn) => fn());
|
|
468
|
-
this.listeners.clear();
|
|
469
|
-
this.settle();
|
|
470
|
-
}
|
|
471
|
-
// ---------------------------------------------------------------------------
|
|
472
|
-
// Evaluation
|
|
473
|
-
// ---------------------------------------------------------------------------
|
|
474
|
-
overrideFor(key) {
|
|
475
|
-
if (Object.prototype.hasOwnProperty.call(this.urlOverrides, key)) return this.urlOverrides[key];
|
|
476
|
-
if (Object.prototype.hasOwnProperty.call(this.overrides, key)) return this.overrides[key];
|
|
477
|
-
return void 0;
|
|
478
|
-
}
|
|
479
|
-
resolve(key, attrs, track) {
|
|
480
|
-
let res;
|
|
481
|
-
try {
|
|
482
|
-
if (this.cfg.mode === "lazy") this.lazyRefresh();
|
|
483
|
-
const ov = this.overrideFor(key);
|
|
484
|
-
const forced = Object.prototype.hasOwnProperty.call(this.urlOverrides, key);
|
|
485
|
-
const override = ov === void 0 ? null : { key, value: ov, reason: "OVERRIDE", inExperiment: false };
|
|
486
|
-
if (override && (forced || this.cfg.behaviour !== "remoteOverLocal")) res = override;
|
|
487
|
-
else if (this.cfg.behaviour === "localOnly") res = notFound(key);
|
|
488
|
-
else {
|
|
489
|
-
res = this.evaluateRemoteOrLocal(key, attrs);
|
|
490
|
-
if (override && res.reason === "FLAG_NOT_FOUND") res = override;
|
|
491
|
-
}
|
|
492
|
-
} catch (err) {
|
|
493
|
-
res = { key, value: null, reason: "ERROR", inExperiment: false, errorMessage: err instanceof Error ? err.message : String(err) };
|
|
494
|
-
}
|
|
495
|
-
if (track) this.record(res, attrs);
|
|
496
|
-
return res;
|
|
497
|
-
}
|
|
498
|
-
evaluateRemoteOrLocal(key, attrs) {
|
|
499
|
-
if (this.remote) {
|
|
500
|
-
const k = stableStringify(attrs);
|
|
501
|
-
const results = k === this.resultsKey || k === stableStringify(this.getAttributes()) ? this.results : this.resultCache.get(k);
|
|
502
|
-
return results?.flags[key] ?? notFound(key);
|
|
503
|
-
}
|
|
504
|
-
if (!this.payload) return notFound(key);
|
|
505
|
-
return evaluateFlag(this.payload, key, attrs, { now: this.now(), stickyBuckets: this.o.stickyBucketService });
|
|
506
|
-
}
|
|
507
|
-
all(attrs) {
|
|
508
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(this.overrides), ...Object.keys(this.urlOverrides)]);
|
|
509
|
-
if (this.cfg.behaviour !== "localOnly") {
|
|
510
|
-
if (this.remote) for (const k of Object.keys(this.results?.flags ?? {})) keys.add(k);
|
|
511
|
-
else for (const k of Object.keys(this.payload?.flags ?? {})) keys.add(k);
|
|
512
|
-
}
|
|
513
|
-
const out = {};
|
|
514
|
-
for (const k of keys) {
|
|
515
|
-
const v = this.resolve(k, attrs, false).value;
|
|
516
|
-
if (v !== null) out[k] = v;
|
|
517
|
-
}
|
|
518
|
-
return out;
|
|
519
|
-
}
|
|
520
|
-
now() {
|
|
521
|
-
const n = this.o.now?.();
|
|
522
|
-
return n === void 0 ? Date.now() : typeof n === "number" ? n : n.getTime();
|
|
523
|
-
}
|
|
524
|
-
record(res, attrs) {
|
|
525
|
-
try {
|
|
526
|
-
this.o.onFlagEvaluated?.(res);
|
|
527
|
-
} catch (err) {
|
|
528
|
-
this.reportError(err);
|
|
529
|
-
}
|
|
530
|
-
if (res.reason === "FLAG_NOT_FOUND") {
|
|
531
|
-
if (this.ready_ && (this.payload || this.results) && isDev()) this.warnOnce(`missing:${res.key}`, `Flag "${res.key}" does not exist in this environment; serving the fallback.`);
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
if (res.reason === "OVERRIDE" || res.reason === "ERROR") return;
|
|
535
|
-
if (this.cfg.reportUsage) {
|
|
536
|
-
const value = (JSON.stringify(res.value) ?? "null").slice(0, 200);
|
|
537
|
-
const k = `${res.key}\0${res.variationId ?? ""}\0${res.variationId ? "" : value}`;
|
|
538
|
-
const u = this.usage.get(k);
|
|
539
|
-
if (u) u.count++;
|
|
540
|
-
else if (this.usage.size < MAX_QUEUE) this.usage.set(k, { flag: res.key, ...res.variationId ? { variationId: res.variationId } : {}, value, count: 1 });
|
|
541
|
-
this.scheduleFlush(USAGE_FLUSH_MS);
|
|
542
|
-
}
|
|
543
|
-
const exp = res.inExperiment ? res.experiment : void 0;
|
|
544
|
-
if (!exp) return;
|
|
545
|
-
const dedupe = `${exp.key}\0${exp.hashValue}\0${exp.variationId}`;
|
|
546
|
-
if (this.exposed.has(dedupe)) return;
|
|
547
|
-
if (this.exposed.size >= 5e4) this.exposed.clear();
|
|
548
|
-
this.exposed.add(dedupe);
|
|
549
|
-
try {
|
|
550
|
-
this.o.trackingCallback?.(exp, res);
|
|
551
|
-
} catch (err) {
|
|
552
|
-
this.reportError(err);
|
|
553
|
-
}
|
|
554
|
-
if (!this.cfg.trackExposures) return;
|
|
555
|
-
const userId = textOf(attrs.id);
|
|
556
|
-
const anonymousId = textOf(attrs.anonymousId);
|
|
557
|
-
if (this.exposures.length < MAX_QUEUE) {
|
|
558
|
-
this.exposures.push({ experiment: exp.key, flag: res.key, variationId: exp.variationId, hashAttribute: exp.hashAttribute, hashValue: exp.hashValue, ...userId ? { userId } : {}, ...anonymousId ? { anonymousId } : {}, timestamp: new Date(this.now()).toISOString() });
|
|
559
|
-
}
|
|
560
|
-
this.scheduleFlush(EXPOSURE_FLUSH_MS);
|
|
561
|
-
}
|
|
562
|
-
warnOnce(id, message) {
|
|
563
|
-
if (this.warned.has(id)) return;
|
|
564
|
-
this.warned.add(id);
|
|
565
|
-
this.log.warn?.(message);
|
|
566
|
-
}
|
|
567
|
-
// ---------------------------------------------------------------------------
|
|
568
|
-
// Data
|
|
569
|
-
// ---------------------------------------------------------------------------
|
|
570
|
-
setPayload(payload, raw, etag, emit) {
|
|
571
|
-
const same = this.payload && this.payload.version === payload.version && this.payload.generatedAt === payload.generatedAt && this.payload.environment === payload.environment;
|
|
572
|
-
this.payload = payload;
|
|
573
|
-
this.raw = raw;
|
|
574
|
-
this.etag = etag;
|
|
575
|
-
if (!emit || same) return;
|
|
576
|
-
try {
|
|
577
|
-
this.o.onChange?.(payload);
|
|
578
|
-
} catch (err) {
|
|
579
|
-
this.reportError(err);
|
|
580
|
-
}
|
|
581
|
-
this.notify();
|
|
582
|
-
}
|
|
583
|
-
setResults(results, key, emit) {
|
|
584
|
-
this.resultCache.delete(key);
|
|
585
|
-
this.resultCache.set(key, results);
|
|
586
|
-
while (this.resultCache.size > MAX_RESULT_CACHE) this.resultCache.delete(this.resultCache.keys().next().value);
|
|
587
|
-
if (key !== stableStringify(this.getAttributes())) return;
|
|
588
|
-
const same = this.results && stableStringify(this.results) === stableStringify(results);
|
|
589
|
-
this.results = results;
|
|
590
|
-
this.resultsKey = key;
|
|
591
|
-
if (!emit || same) return;
|
|
592
|
-
try {
|
|
593
|
-
this.o.onChange?.(results);
|
|
594
|
-
} catch (err) {
|
|
595
|
-
this.reportError(err);
|
|
596
|
-
}
|
|
597
|
-
this.notify();
|
|
598
|
-
}
|
|
599
|
-
/** Decrypts when needed and applies. Returns false (keeping the previous payload) when unusable. */
|
|
600
|
-
async applyRaw(raw, etag) {
|
|
601
|
-
if (isEncryptedPayload(raw)) {
|
|
602
|
-
if (!this.o.decryptionKey) {
|
|
603
|
-
this.reportError(new Error("The payload is encrypted but no `decryptionKey` was given; keeping the previous flags."));
|
|
604
|
-
return false;
|
|
605
|
-
}
|
|
606
|
-
try {
|
|
607
|
-
const plain = JSON.parse(await decryptPayloadString(raw.encryptedPayload, this.o.decryptionKey));
|
|
608
|
-
const envelope = { ...raw };
|
|
609
|
-
delete envelope.encryptedPayload;
|
|
610
|
-
const merged = { ...envelope, ...plain };
|
|
611
|
-
if (!isFlagsPayload(merged)) throw new Error("decrypted payload has no flags");
|
|
612
|
-
this.setPayload(merged, raw, etag, true);
|
|
613
|
-
return true;
|
|
614
|
-
} catch (err) {
|
|
615
|
-
this.reportError(new Error(`Could not decrypt the flags payload (${err instanceof Error ? err.message : String(err)}); keeping the previous flags.`));
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
if (!isFlagsPayload(raw)) {
|
|
620
|
-
this.reportError(new Error("Unexpected flags payload; keeping the previous flags."));
|
|
621
|
-
return false;
|
|
622
|
-
}
|
|
623
|
-
this.setPayload(raw, raw, etag, true);
|
|
624
|
-
return true;
|
|
625
|
-
}
|
|
626
|
-
async loadStored() {
|
|
627
|
-
if (!this.storage) return;
|
|
628
|
-
try {
|
|
629
|
-
const text = await this.storage.getItem(this.storageKey);
|
|
630
|
-
if (!text || this.payload) return;
|
|
631
|
-
const saved = JSON.parse(text);
|
|
632
|
-
if (saved?.payload && !this.payload) await this.applyRaw(saved.payload, saved.etag ?? null);
|
|
633
|
-
} catch (err) {
|
|
634
|
-
this.log.debug?.("could not read cached flags", err);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
persist() {
|
|
638
|
-
if (!this.storage || !this.raw) return;
|
|
639
|
-
try {
|
|
640
|
-
void Promise.resolve(this.storage.setItem(this.storageKey, JSON.stringify({ payload: this.raw, etag: this.etag, savedAt: (/* @__PURE__ */ new Date()).toISOString() }))).catch(() => {
|
|
641
|
-
});
|
|
642
|
-
} catch {
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
// ---------------------------------------------------------------------------
|
|
646
|
-
// Network
|
|
647
|
-
// ---------------------------------------------------------------------------
|
|
648
|
-
url(path) {
|
|
649
|
-
const q = this.secret ? `environment=${encodeURIComponent(this.environment ?? "")}` : `key=${encodeURIComponent(this.sdkKey)}`;
|
|
650
|
-
return `${this.apiUrl}/api/v1/sdk${path}?${q}`;
|
|
651
|
-
}
|
|
652
|
-
headers(extra = {}) {
|
|
653
|
-
return { ...extra, ...this.secret ? { Authorization: `Bearer ${this.sdkKey}` } : { "X-Guidely-Key": this.sdkKey } };
|
|
654
|
-
}
|
|
655
|
-
get fetcher() {
|
|
656
|
-
if (this.o.fetch) return this.o.fetch;
|
|
657
|
-
return typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
658
|
-
}
|
|
659
|
-
async request(method, path, opts = {}) {
|
|
660
|
-
const f = this.fetcher;
|
|
661
|
-
if (!f) throw new Error("fetch is not available; pass `fetch`");
|
|
662
|
-
const headers = this.headers({ Accept: "application/json" });
|
|
663
|
-
if (opts.body !== void 0) headers["Content-Type"] = "application/json";
|
|
664
|
-
if (opts.etag) headers["If-None-Match"] = opts.etag;
|
|
665
|
-
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
666
|
-
const t = ac ? timer(() => ac.abort(), opts.timeoutMs ?? 1e4) : null;
|
|
667
|
-
try {
|
|
668
|
-
const browserOnly = isBrowser() ? { credentials: "omit", keepalive: opts.keepalive } : {};
|
|
669
|
-
const res = await f(this.url(path), { method, headers, body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body), signal: ac?.signal, ...browserOnly });
|
|
670
|
-
let data = null;
|
|
671
|
-
if (res.status !== 304 && res.status !== 204) {
|
|
672
|
-
try {
|
|
673
|
-
data = await res.json();
|
|
674
|
-
} catch {
|
|
675
|
-
data = null;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
679
|
-
} finally {
|
|
680
|
-
if (t) clearTimeout(t);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
fetchNow() {
|
|
684
|
-
if (this.destroyed || this.offline || this.cfg.behaviour === "localOnly") return Promise.resolve(false);
|
|
685
|
-
if (this.inflight) return this.inflight;
|
|
686
|
-
this.inflight = (this.remote ? this.fetchRemote() : this.fetchPayload()).catch((err) => {
|
|
687
|
-
this.reportError(err);
|
|
688
|
-
return false;
|
|
689
|
-
}).then((ok) => {
|
|
690
|
-
this.inflight = null;
|
|
691
|
-
this.lastFetchAt = Date.now();
|
|
692
|
-
this.failures = ok ? 0 : this.failures + 1;
|
|
693
|
-
this.retryAt = ok ? 0 : Date.now() + backoff(this.failures, Math.max(this.cfg.pollMs, MAX_BACKOFF_MS));
|
|
694
|
-
if (ok) this.lastError = "";
|
|
695
|
-
return ok;
|
|
696
|
-
});
|
|
697
|
-
return this.inflight;
|
|
698
|
-
}
|
|
699
|
-
async fetchPayload() {
|
|
700
|
-
let res;
|
|
701
|
-
try {
|
|
702
|
-
res = await this.request("GET", "/flags", { etag: this.payload ? this.etag : null });
|
|
703
|
-
} catch (err) {
|
|
704
|
-
this.reportError(new Error(`Flags request failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
707
|
-
if (res.status === 304) return true;
|
|
708
|
-
const code = res.data?.error?.code;
|
|
709
|
-
if (res.status === 403 && code === "remote_evaluation_required" && !this.secret) {
|
|
710
|
-
this.log.warn?.("This environment requires remote evaluation; switching to `remoteEval: true`.");
|
|
711
|
-
this.remote = true;
|
|
712
|
-
return this.fetchRemote();
|
|
713
|
-
}
|
|
714
|
-
if (!res.ok || res.data == null) {
|
|
715
|
-
this.reportError(new Error(`Flags request failed (${res.status}${code ? ` ${code}` : ""})${this.payload ? "; serving the last good flags" : ""}`));
|
|
716
|
-
return false;
|
|
717
|
-
}
|
|
718
|
-
const applied = await this.applyRaw(res.data, res.etag);
|
|
719
|
-
if (applied) this.persist();
|
|
720
|
-
return applied;
|
|
721
|
-
}
|
|
722
|
-
async fetchRemote() {
|
|
723
|
-
const attrs = this.getAttributes();
|
|
724
|
-
const key = stableStringify(attrs);
|
|
725
|
-
let res;
|
|
726
|
-
try {
|
|
727
|
-
res = await this.request("POST", "/flags/evaluate", { body: { attributes: attrs } });
|
|
728
|
-
} catch (err) {
|
|
729
|
-
this.reportError(new Error(`Remote evaluation failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
732
|
-
if (!res.ok || !res.data || !isRemoteResults(res.data)) {
|
|
733
|
-
this.reportError(new Error(`Remote evaluation failed (${res.status})`));
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
this.setResults(res.data, key, true);
|
|
737
|
-
return true;
|
|
738
|
-
}
|
|
739
|
-
scheduleRemote() {
|
|
740
|
-
if (this.destroyed || this.offline || this.cfg.behaviour === "localOnly") return;
|
|
741
|
-
if (this.remoteTimer) clearTimeout(this.remoteTimer);
|
|
742
|
-
this.remoteTimer = timer(() => {
|
|
743
|
-
this.remoteTimer = null;
|
|
744
|
-
void this.fetchRemote().then((ok) => {
|
|
745
|
-
if (!ok) this.failures++;
|
|
746
|
-
});
|
|
747
|
-
}, this.cfg.debounceMs);
|
|
748
|
-
}
|
|
749
|
-
lazyRefresh() {
|
|
750
|
-
if (this.inflight || this.offline || this.destroyed || this.cfg.behaviour === "localOnly") return;
|
|
751
|
-
const now = Date.now();
|
|
752
|
-
if (now - this.lastFetchAt < this.cfg.ttlMs || now < this.retryAt) return;
|
|
753
|
-
this.lastFetchAt = now;
|
|
754
|
-
void this.fetchNow();
|
|
755
|
-
}
|
|
756
|
-
async start() {
|
|
757
|
-
const boot = this.o.payload;
|
|
758
|
-
if (isEncryptedPayload(boot)) await this.applyRaw(boot, null);
|
|
759
|
-
if (!this.payload && !this.results && !this.remote) await this.loadStored();
|
|
760
|
-
if (this.destroyed) return;
|
|
761
|
-
const bootstrapped = !!(boot || this.o.remoteResults) && !!(this.payload || this.results);
|
|
762
|
-
if (this.cfg.behaviour === "localOnly" || this.offline || this.cfg.mode === "manual") return this.settle();
|
|
763
|
-
if (bootstrapped) {
|
|
764
|
-
this.settle();
|
|
765
|
-
await this.fetchNow();
|
|
766
|
-
return this.schedule();
|
|
767
|
-
}
|
|
768
|
-
await this.fetchNow();
|
|
769
|
-
this.settle();
|
|
770
|
-
this.schedule();
|
|
771
|
-
}
|
|
772
|
-
settle() {
|
|
773
|
-
if (this.ready_) return;
|
|
774
|
-
this.ready_ = true;
|
|
775
|
-
this.resolveReady();
|
|
776
|
-
try {
|
|
777
|
-
this.o.onReady?.();
|
|
778
|
-
} catch (err) {
|
|
779
|
-
this.reportError(err);
|
|
780
|
-
}
|
|
781
|
-
this.notify();
|
|
782
|
-
}
|
|
783
|
-
/** Arms the poll timer / stream for the current mode. */
|
|
784
|
-
schedule() {
|
|
785
|
-
if (this.destroyed || this.offline || this.cfg.behaviour === "localOnly") return;
|
|
786
|
-
if (this.cfg.mode === "auto") this.schedulePoll();
|
|
787
|
-
else if (this.cfg.mode === "stream") {
|
|
788
|
-
if (!this.stream) this.openStream();
|
|
789
|
-
if (!this.streamConnected) this.schedulePoll();
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
schedulePoll() {
|
|
793
|
-
if (this.pollTimer) clearTimeout(this.pollTimer);
|
|
794
|
-
const delay = this.failures ? Math.max(0, this.retryAt - Date.now()) : this.cfg.pollMs;
|
|
795
|
-
this.pollTimer = timer(() => {
|
|
796
|
-
this.pollTimer = null;
|
|
797
|
-
if (this.cfg.mode === "stream" && this.streamConnected) return;
|
|
798
|
-
if (isBrowser() && document.visibilityState === "hidden") return this.schedulePoll();
|
|
799
|
-
void this.fetchNow().then(() => this.schedule());
|
|
800
|
-
}, delay);
|
|
801
|
-
}
|
|
802
|
-
stopNetwork() {
|
|
803
|
-
for (const t of [this.pollTimer, this.remoteTimer, this.reconnectTimer]) if (t) clearTimeout(t);
|
|
804
|
-
this.pollTimer = this.remoteTimer = this.reconnectTimer = null;
|
|
805
|
-
this.stream?.close();
|
|
806
|
-
this.stream = null;
|
|
807
|
-
this.streamConnected = false;
|
|
808
|
-
}
|
|
809
|
-
// ---------------------------------------------------------------------------
|
|
810
|
-
// Streaming
|
|
811
|
-
// ---------------------------------------------------------------------------
|
|
812
|
-
onStreamEvent(event, data) {
|
|
813
|
-
if (event !== "flags") return;
|
|
814
|
-
let version;
|
|
815
|
-
try {
|
|
816
|
-
version = JSON.parse(data).version;
|
|
817
|
-
} catch {
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const current = this.remote ? this.results?.version ?? -1 : this.payload?.version ?? -1;
|
|
821
|
-
if (typeof version === "number" && version > current) void this.fetchNow();
|
|
822
|
-
}
|
|
823
|
-
openStream() {
|
|
824
|
-
if (this.destroyed || this.offline || this.stream) return;
|
|
825
|
-
const url = this.url("/flags/stream");
|
|
826
|
-
const ES = globalThis.EventSource;
|
|
827
|
-
const opened = () => {
|
|
828
|
-
this.streamConnected = true;
|
|
829
|
-
this.streamFailures = 0;
|
|
830
|
-
if (this.pollTimer) clearTimeout(this.pollTimer);
|
|
831
|
-
this.pollTimer = null;
|
|
832
|
-
};
|
|
833
|
-
if (ES && !this.secret) {
|
|
834
|
-
const es = new ES(url);
|
|
835
|
-
es.addEventListener("flags", (e) => this.onStreamEvent("flags", e.data));
|
|
836
|
-
es.onopen = opened;
|
|
837
|
-
es.onerror = () => {
|
|
838
|
-
es.close();
|
|
839
|
-
if (this.stream === handle2) this.streamDown();
|
|
840
|
-
};
|
|
841
|
-
const handle2 = { close: () => es.close() };
|
|
842
|
-
this.stream = handle2;
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
const f = this.fetcher;
|
|
846
|
-
if (!f || typeof TextDecoder === "undefined") return this.streamDown(true);
|
|
847
|
-
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
848
|
-
const handle = { close: () => ac?.abort() };
|
|
849
|
-
this.stream = handle;
|
|
850
|
-
void (async () => {
|
|
851
|
-
try {
|
|
852
|
-
const res = await f(url, { headers: this.headers({ Accept: "text/event-stream" }), signal: ac?.signal, ...isBrowser() ? { credentials: "omit" } : {} });
|
|
853
|
-
const reader = res.ok ? res.body?.getReader?.() : void 0;
|
|
854
|
-
if (!reader) {
|
|
855
|
-
if (this.stream === handle) this.streamDown(res.ok);
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
opened();
|
|
859
|
-
const parser = createSseParser((event, data) => this.onStreamEvent(event, data));
|
|
860
|
-
const decoder = new TextDecoder();
|
|
861
|
-
for (; ; ) {
|
|
862
|
-
const { done, value } = await reader.read();
|
|
863
|
-
if (value) parser.feed(decoder.decode(value, { stream: true }));
|
|
864
|
-
if (done) break;
|
|
865
|
-
}
|
|
866
|
-
parser.end();
|
|
867
|
-
} catch {
|
|
868
|
-
}
|
|
869
|
-
if (this.stream === handle) this.streamDown();
|
|
870
|
-
})();
|
|
871
|
-
}
|
|
872
|
-
/** The stream dropped: poll meanwhile and reconnect with backoff (`permanent`: never retry). */
|
|
873
|
-
streamDown(permanent = false) {
|
|
874
|
-
this.stream = null;
|
|
875
|
-
this.streamConnected = false;
|
|
876
|
-
if (this.destroyed || this.offline) return;
|
|
877
|
-
this.schedulePoll();
|
|
878
|
-
if (permanent) return;
|
|
879
|
-
this.streamFailures++;
|
|
880
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
881
|
-
this.reconnectTimer = timer(() => {
|
|
882
|
-
this.reconnectTimer = null;
|
|
883
|
-
this.openStream();
|
|
884
|
-
void this.fetchNow();
|
|
885
|
-
}, backoff(this.streamFailures, MAX_BACKOFF_MS));
|
|
886
|
-
}
|
|
887
|
-
// ---------------------------------------------------------------------------
|
|
888
|
-
// Usage + exposures
|
|
889
|
-
// ---------------------------------------------------------------------------
|
|
890
|
-
scheduleFlush(ms) {
|
|
891
|
-
if (this.destroyed) return;
|
|
892
|
-
const due = Date.now() + ms;
|
|
893
|
-
if (this.flushTimer && due >= this.flushDue) return;
|
|
894
|
-
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
895
|
-
this.flushDue = due;
|
|
896
|
-
this.flushTimer = timer(() => {
|
|
897
|
-
this.flushTimer = null;
|
|
898
|
-
this.flushDue = Infinity;
|
|
899
|
-
if (!this.offline) void this.flush();
|
|
900
|
-
else if (this.usage.size || this.exposures.length) this.scheduleFlush(USAGE_FLUSH_MS);
|
|
901
|
-
}, ms);
|
|
902
|
-
}
|
|
903
|
-
takeBatch() {
|
|
904
|
-
if (!this.usage.size && !this.exposures.length) return null;
|
|
905
|
-
const batch = { sdk: this.o.sdk ?? { name: FLAGS_SDK_NAME, version: FLAGS_SDK_VERSION } };
|
|
906
|
-
if (this.usage.size) batch.usage = Array.from(this.usage.values());
|
|
907
|
-
if (this.exposures.length) batch.exposures = this.exposures;
|
|
908
|
-
this.usage.clear();
|
|
909
|
-
this.exposures = [];
|
|
910
|
-
return batch;
|
|
911
|
-
}
|
|
912
|
-
requeue(batch) {
|
|
913
|
-
for (const u of batch.usage ?? []) {
|
|
914
|
-
const k = `${u.flag}\0${u.variationId ?? ""}\0${u.variationId ? "" : u.value ?? ""}`;
|
|
915
|
-
const cur = this.usage.get(k);
|
|
916
|
-
if (cur) cur.count += u.count;
|
|
917
|
-
else if (this.usage.size < MAX_QUEUE) this.usage.set(k, { flag: u.flag, variationId: u.variationId, value: u.value ?? "", count: u.count });
|
|
918
|
-
}
|
|
919
|
-
this.exposures = [...batch.exposures ?? [], ...this.exposures].slice(0, MAX_QUEUE);
|
|
920
|
-
this.scheduleFlush(USAGE_FLUSH_MS);
|
|
921
|
-
}
|
|
922
|
-
/** Page hide / destroy: `sendBeacon` for public keys, else a `keepalive` fetch. */
|
|
923
|
-
flushOnExit() {
|
|
924
|
-
if (this.offline) return;
|
|
925
|
-
const batch = this.takeBatch();
|
|
926
|
-
if (!batch) return;
|
|
927
|
-
try {
|
|
928
|
-
if (!this.secret && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && typeof Blob !== "undefined") {
|
|
929
|
-
if (navigator.sendBeacon(this.url("/flags/events"), new Blob([JSON.stringify(batch)], { type: "text/plain;charset=UTF-8" }))) return;
|
|
930
|
-
}
|
|
931
|
-
} catch {
|
|
932
|
-
}
|
|
933
|
-
void this.request("POST", "/flags/events", { body: batch, keepalive: true }).catch(() => {
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
wireBrowser() {
|
|
937
|
-
if (!isBrowser()) return;
|
|
938
|
-
const onHide = () => this.flushOnExit();
|
|
939
|
-
const onVisibility = () => {
|
|
940
|
-
if (document.visibilityState === "hidden") return this.flushOnExit();
|
|
941
|
-
if ((this.cfg.mode === "auto" || this.cfg.mode === "stream" && !this.streamConnected) && Date.now() - this.lastFetchAt > this.cfg.pollMs) void this.fetchNow().then(() => this.schedule());
|
|
942
|
-
};
|
|
943
|
-
window.addEventListener("pagehide", onHide);
|
|
944
|
-
document.addEventListener("visibilitychange", onVisibility);
|
|
945
|
-
this.cleanups.push(() => {
|
|
946
|
-
window.removeEventListener("pagehide", onHide);
|
|
947
|
-
document.removeEventListener("visibilitychange", onVisibility);
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
// ---------------------------------------------------------------------------
|
|
951
|
-
notify() {
|
|
952
|
-
for (const l of Array.from(this.listeners)) {
|
|
953
|
-
try {
|
|
954
|
-
l();
|
|
955
|
-
} catch (err) {
|
|
956
|
-
this.reportError(err);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
reportError(err) {
|
|
961
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
962
|
-
if (error.message !== this.lastError) this.log.error?.(error.message);
|
|
963
|
-
this.lastError = error.message;
|
|
964
|
-
try {
|
|
965
|
-
this.o.onError?.(error);
|
|
966
|
-
} catch {
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
};
|
|
970
|
-
function createFlagsClient(options) {
|
|
971
|
-
return new FlagsClient(options);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
export {
|
|
975
|
-
decryptPayloadString,
|
|
976
|
-
stableStringify,
|
|
977
|
-
isEncryptedPayload,
|
|
978
|
-
isRemoteResults,
|
|
979
|
-
isFlagsPayload,
|
|
980
|
-
parseUrlOverrides,
|
|
981
|
-
createStaticFlags,
|
|
982
|
-
FLAGS_SDK_NAME,
|
|
983
|
-
FLAGS_SDK_VERSION,
|
|
984
|
-
DEFAULT_FLAGS_API_URL,
|
|
985
|
-
FlagsClient,
|
|
986
|
-
createFlagsClient
|
|
987
|
-
};
|
|
988
|
-
//# sourceMappingURL=chunk-ZTY7K7P2.js.map
|