@flipswitch-io/sdk 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/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/index.d.mts +305 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +645 -0
- package/dist/index.mjs +616 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FlagCache: () => FlagCache,
|
|
24
|
+
FlipswitchProvider: () => FlipswitchProvider,
|
|
25
|
+
SseClient: () => SseClient
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/provider.ts
|
|
30
|
+
var import_ofrep_web_provider = require("@openfeature/ofrep-web-provider");
|
|
31
|
+
|
|
32
|
+
// src/sse-client.ts
|
|
33
|
+
var MIN_RETRY_DELAY = 1e3;
|
|
34
|
+
var MAX_RETRY_DELAY = 3e4;
|
|
35
|
+
var SseClient = class {
|
|
36
|
+
constructor(baseUrl, apiKey, onFlagChange, onStatusChange, telemetryHeaders) {
|
|
37
|
+
this.baseUrl = baseUrl;
|
|
38
|
+
this.apiKey = apiKey;
|
|
39
|
+
this.onFlagChange = onFlagChange;
|
|
40
|
+
this.onStatusChange = onStatusChange;
|
|
41
|
+
this.telemetryHeaders = telemetryHeaders;
|
|
42
|
+
this.eventSource = null;
|
|
43
|
+
this.retryDelay = MIN_RETRY_DELAY;
|
|
44
|
+
this.reconnectTimeout = null;
|
|
45
|
+
this.closed = false;
|
|
46
|
+
this.status = "disconnected";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Start the SSE connection.
|
|
50
|
+
*/
|
|
51
|
+
connect() {
|
|
52
|
+
if (this.closed) return;
|
|
53
|
+
if (this.eventSource) {
|
|
54
|
+
this.eventSource.close();
|
|
55
|
+
}
|
|
56
|
+
this.updateStatus("connecting");
|
|
57
|
+
const url = `${this.baseUrl}/api/v1/flags/events`;
|
|
58
|
+
try {
|
|
59
|
+
if (typeof EventSource !== "undefined") {
|
|
60
|
+
this.connectWithFetch(url);
|
|
61
|
+
} else {
|
|
62
|
+
this.connectWithPolyfill(url);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error("[Flipswitch] Failed to establish SSE connection:", error);
|
|
66
|
+
this.updateStatus("error");
|
|
67
|
+
this.scheduleReconnect();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Connect using fetch-based SSE (supports custom headers).
|
|
72
|
+
*/
|
|
73
|
+
async connectWithFetch(url) {
|
|
74
|
+
try {
|
|
75
|
+
const headers = {
|
|
76
|
+
"X-API-Key": this.apiKey,
|
|
77
|
+
Accept: "text/event-stream",
|
|
78
|
+
"Cache-Control": "no-cache",
|
|
79
|
+
...this.telemetryHeaders
|
|
80
|
+
};
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
headers
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`SSE connection failed: ${response.status}`);
|
|
87
|
+
}
|
|
88
|
+
if (!response.body) {
|
|
89
|
+
throw new Error("Response body is null");
|
|
90
|
+
}
|
|
91
|
+
this.updateStatus("connected");
|
|
92
|
+
this.retryDelay = MIN_RETRY_DELAY;
|
|
93
|
+
const reader = response.body.getReader();
|
|
94
|
+
const decoder = new TextDecoder();
|
|
95
|
+
let buffer = "";
|
|
96
|
+
const processStream = async () => {
|
|
97
|
+
while (!this.closed) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done) {
|
|
100
|
+
this.updateStatus("disconnected");
|
|
101
|
+
this.scheduleReconnect();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
buffer += decoder.decode(value, { stream: true });
|
|
105
|
+
const lines = buffer.split("\n");
|
|
106
|
+
buffer = lines.pop() || "";
|
|
107
|
+
let eventType = "";
|
|
108
|
+
let eventData = "";
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (line.startsWith("event:")) {
|
|
111
|
+
eventType = line.slice(6).trim();
|
|
112
|
+
} else if (line.startsWith("data:")) {
|
|
113
|
+
eventData = line.slice(5).trim();
|
|
114
|
+
} else if (line === "" && eventData) {
|
|
115
|
+
this.handleEvent(eventType, eventData);
|
|
116
|
+
eventType = "";
|
|
117
|
+
eventData = "";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
processStream().catch((error) => {
|
|
123
|
+
if (!this.closed) {
|
|
124
|
+
console.error("[Flipswitch] SSE stream error:", error);
|
|
125
|
+
this.updateStatus("error");
|
|
126
|
+
this.scheduleReconnect();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!this.closed) {
|
|
131
|
+
console.error("[Flipswitch] SSE connection error:", error);
|
|
132
|
+
this.updateStatus("error");
|
|
133
|
+
this.scheduleReconnect();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Connect using native EventSource (for environments that support it).
|
|
139
|
+
* Note: This requires server-side support for API key in query params.
|
|
140
|
+
*/
|
|
141
|
+
connectWithPolyfill(url) {
|
|
142
|
+
this.connectWithFetch(url);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Handle incoming SSE events.
|
|
146
|
+
*/
|
|
147
|
+
handleEvent(eventType, data) {
|
|
148
|
+
if (eventType === "heartbeat") {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (eventType === "flag-change") {
|
|
152
|
+
try {
|
|
153
|
+
const event = JSON.parse(data);
|
|
154
|
+
this.onFlagChange(event);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error("[Flipswitch] Failed to parse flag-change event:", error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
162
|
+
*/
|
|
163
|
+
scheduleReconnect() {
|
|
164
|
+
if (this.closed) return;
|
|
165
|
+
if (this.reconnectTimeout) {
|
|
166
|
+
clearTimeout(this.reconnectTimeout);
|
|
167
|
+
}
|
|
168
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
169
|
+
if (!this.closed) {
|
|
170
|
+
this.connect();
|
|
171
|
+
this.retryDelay = Math.min(this.retryDelay * 2, MAX_RETRY_DELAY);
|
|
172
|
+
}
|
|
173
|
+
}, this.retryDelay);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Update and broadcast connection status.
|
|
177
|
+
*/
|
|
178
|
+
updateStatus(status) {
|
|
179
|
+
this.status = status;
|
|
180
|
+
this.onStatusChange?.(status);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get current connection status.
|
|
184
|
+
*/
|
|
185
|
+
getStatus() {
|
|
186
|
+
return this.status;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Close the SSE connection and stop reconnection attempts.
|
|
190
|
+
*/
|
|
191
|
+
close() {
|
|
192
|
+
this.closed = true;
|
|
193
|
+
this.updateStatus("disconnected");
|
|
194
|
+
if (this.reconnectTimeout) {
|
|
195
|
+
clearTimeout(this.reconnectTimeout);
|
|
196
|
+
this.reconnectTimeout = null;
|
|
197
|
+
}
|
|
198
|
+
if (this.eventSource) {
|
|
199
|
+
this.eventSource.close();
|
|
200
|
+
this.eventSource = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/provider.ts
|
|
206
|
+
var DEFAULT_BASE_URL = "https://api.flipswitch.io";
|
|
207
|
+
var SDK_VERSION = "0.1.0";
|
|
208
|
+
var FlipswitchProvider = class {
|
|
209
|
+
constructor(options, eventHandlers) {
|
|
210
|
+
this.metadata = {
|
|
211
|
+
name: "flipswitch"
|
|
212
|
+
};
|
|
213
|
+
this.rulesFromFlagValue = false;
|
|
214
|
+
this.sseClient = null;
|
|
215
|
+
this._status = "NOT_READY";
|
|
216
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
217
|
+
this.userEventHandlers = {};
|
|
218
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
219
|
+
this.apiKey = options.apiKey;
|
|
220
|
+
this.enableRealtime = options.enableRealtime ?? true;
|
|
221
|
+
this.enableTelemetry = options.enableTelemetry ?? true;
|
|
222
|
+
this.fetchImpl = options.fetchImplementation ?? (typeof window !== "undefined" ? fetch.bind(window) : fetch);
|
|
223
|
+
this.userEventHandlers = eventHandlers ?? {};
|
|
224
|
+
const headers = [["X-API-Key", this.apiKey]];
|
|
225
|
+
if (this.enableTelemetry) {
|
|
226
|
+
headers.push(["X-Flipswitch-SDK", this.getTelemetrySdkHeader()]);
|
|
227
|
+
headers.push(["X-Flipswitch-Runtime", this.getTelemetryRuntimeHeader()]);
|
|
228
|
+
headers.push(["X-Flipswitch-OS", this.getTelemetryOsHeader()]);
|
|
229
|
+
headers.push(["X-Flipswitch-Features", this.getTelemetryFeaturesHeader()]);
|
|
230
|
+
}
|
|
231
|
+
this.ofrepProvider = new import_ofrep_web_provider.OFREPWebProvider({
|
|
232
|
+
baseUrl: this.baseUrl + "/ofrep/v1",
|
|
233
|
+
fetchImplementation: this.fetchImpl,
|
|
234
|
+
headers
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
getTelemetrySdkHeader() {
|
|
238
|
+
return `javascript/${SDK_VERSION}`;
|
|
239
|
+
}
|
|
240
|
+
getTelemetryRuntimeHeader() {
|
|
241
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
242
|
+
return `node/${process.versions.node}`;
|
|
243
|
+
}
|
|
244
|
+
if (typeof navigator !== "undefined") {
|
|
245
|
+
const ua = navigator.userAgent;
|
|
246
|
+
if (ua.includes("Chrome")) {
|
|
247
|
+
const match = ua.match(/Chrome\/(\d+)/);
|
|
248
|
+
return `chrome/${match?.[1] ?? "unknown"}`;
|
|
249
|
+
}
|
|
250
|
+
if (ua.includes("Firefox")) {
|
|
251
|
+
const match = ua.match(/Firefox\/(\d+)/);
|
|
252
|
+
return `firefox/${match?.[1] ?? "unknown"}`;
|
|
253
|
+
}
|
|
254
|
+
if (ua.includes("Safari") && !ua.includes("Chrome")) {
|
|
255
|
+
const match = ua.match(/Version\/(\d+)/);
|
|
256
|
+
return `safari/${match?.[1] ?? "unknown"}`;
|
|
257
|
+
}
|
|
258
|
+
return "browser/unknown";
|
|
259
|
+
}
|
|
260
|
+
return "unknown/unknown";
|
|
261
|
+
}
|
|
262
|
+
getTelemetryOsHeader() {
|
|
263
|
+
if (typeof process !== "undefined" && process.platform) {
|
|
264
|
+
const platform = process.platform;
|
|
265
|
+
const arch = process.arch;
|
|
266
|
+
const os = platform === "darwin" ? "darwin" : platform === "win32" ? "windows" : platform;
|
|
267
|
+
return `${os}/${arch}`;
|
|
268
|
+
}
|
|
269
|
+
if (typeof navigator !== "undefined") {
|
|
270
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
271
|
+
let os = "unknown";
|
|
272
|
+
let arch = "unknown";
|
|
273
|
+
if (ua.includes("mac")) os = "darwin";
|
|
274
|
+
else if (ua.includes("win")) os = "windows";
|
|
275
|
+
else if (ua.includes("linux")) os = "linux";
|
|
276
|
+
else if (ua.includes("android")) os = "android";
|
|
277
|
+
else if (ua.includes("iphone") || ua.includes("ipad")) os = "ios";
|
|
278
|
+
if (ua.includes("arm64") || ua.includes("aarch64")) arch = "arm64";
|
|
279
|
+
else if (ua.includes("x64") || ua.includes("x86_64") || ua.includes("amd64")) arch = "amd64";
|
|
280
|
+
return `${os}/${arch}`;
|
|
281
|
+
}
|
|
282
|
+
return "unknown/unknown";
|
|
283
|
+
}
|
|
284
|
+
getTelemetryFeaturesHeader() {
|
|
285
|
+
return `sse=${this.enableRealtime}`;
|
|
286
|
+
}
|
|
287
|
+
getTelemetryHeaders() {
|
|
288
|
+
if (!this.enableTelemetry) return {};
|
|
289
|
+
return {
|
|
290
|
+
"X-Flipswitch-SDK": this.getTelemetrySdkHeader(),
|
|
291
|
+
"X-Flipswitch-Runtime": this.getTelemetryRuntimeHeader(),
|
|
292
|
+
"X-Flipswitch-OS": this.getTelemetryOsHeader(),
|
|
293
|
+
"X-Flipswitch-Features": this.getTelemetryFeaturesHeader()
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
get status() {
|
|
297
|
+
return this._status;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Initialize the provider.
|
|
301
|
+
* Validates the API key and starts SSE connection if real-time is enabled.
|
|
302
|
+
*/
|
|
303
|
+
async initialize(context) {
|
|
304
|
+
this._status = "NOT_READY";
|
|
305
|
+
try {
|
|
306
|
+
await this.ofrepProvider.initialize(context);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
try {
|
|
309
|
+
const response = await this.fetchImpl(`${this.baseUrl}/ofrep/v1/evaluate/flags`, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: {
|
|
312
|
+
"Content-Type": "application/json",
|
|
313
|
+
"X-API-Key": this.apiKey,
|
|
314
|
+
...this.getTelemetryHeaders()
|
|
315
|
+
},
|
|
316
|
+
body: JSON.stringify({
|
|
317
|
+
context: { targetingKey: "_init_" }
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
if (response.status === 401 || response.status === 403) {
|
|
321
|
+
this._status = "ERROR";
|
|
322
|
+
throw new Error("Invalid API key");
|
|
323
|
+
}
|
|
324
|
+
if (!response.ok && response.status !== 404) {
|
|
325
|
+
this._status = "ERROR";
|
|
326
|
+
throw new Error(`Failed to connect to Flipswitch: ${response.status}`);
|
|
327
|
+
}
|
|
328
|
+
} catch (validationError) {
|
|
329
|
+
this._status = "ERROR";
|
|
330
|
+
throw validationError;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (this.enableRealtime) {
|
|
334
|
+
this.startSseConnection();
|
|
335
|
+
}
|
|
336
|
+
this._status = "READY";
|
|
337
|
+
this.emit("PROVIDER_READY");
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Called when the provider is shut down.
|
|
341
|
+
*/
|
|
342
|
+
async onClose() {
|
|
343
|
+
this.sseClient?.close();
|
|
344
|
+
this.sseClient = null;
|
|
345
|
+
await this.ofrepProvider.onClose?.();
|
|
346
|
+
this._status = "NOT_READY";
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Start the SSE connection for real-time updates.
|
|
350
|
+
*/
|
|
351
|
+
startSseConnection() {
|
|
352
|
+
const telemetryHeaders = this.getTelemetryHeadersMap();
|
|
353
|
+
this.sseClient = new SseClient(
|
|
354
|
+
this.baseUrl,
|
|
355
|
+
this.apiKey,
|
|
356
|
+
(event) => {
|
|
357
|
+
this.handleFlagChange(event);
|
|
358
|
+
},
|
|
359
|
+
(status) => {
|
|
360
|
+
this.userEventHandlers.onConnectionStatusChange?.(status);
|
|
361
|
+
if (status === "error") {
|
|
362
|
+
this._status = "STALE";
|
|
363
|
+
this.emit("PROVIDER_STALE");
|
|
364
|
+
} else if (status === "connected" && this._status === "STALE") {
|
|
365
|
+
this._status = "READY";
|
|
366
|
+
this.emit("PROVIDER_READY");
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
telemetryHeaders
|
|
370
|
+
);
|
|
371
|
+
this.sseClient.connect();
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get telemetry headers as a map.
|
|
375
|
+
*/
|
|
376
|
+
getTelemetryHeadersMap() {
|
|
377
|
+
if (!this.enableTelemetry) {
|
|
378
|
+
return void 0;
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
"X-Flipswitch-SDK": this.getTelemetrySdkHeader(),
|
|
382
|
+
"X-Flipswitch-Runtime": this.getTelemetryRuntimeHeader(),
|
|
383
|
+
"X-Flipswitch-OS": this.getTelemetryOsHeader(),
|
|
384
|
+
"X-Flipswitch-Features": this.getTelemetryFeaturesHeader()
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Handle a flag change event from SSE.
|
|
389
|
+
* Emits PROVIDER_CONFIGURATION_CHANGED to trigger re-evaluation.
|
|
390
|
+
*/
|
|
391
|
+
handleFlagChange(event) {
|
|
392
|
+
this.userEventHandlers.onFlagChange?.(event);
|
|
393
|
+
this.emit("PROVIDER_CONFIGURATION_CHANGED");
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Emit an event to registered handlers.
|
|
397
|
+
*/
|
|
398
|
+
emit(event) {
|
|
399
|
+
const handlers = this.eventHandlers.get(event);
|
|
400
|
+
if (handlers) {
|
|
401
|
+
Array.from(handlers).forEach((handler) => handler());
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Register an event handler.
|
|
406
|
+
*/
|
|
407
|
+
onProviderEvent(event, handler) {
|
|
408
|
+
if (!this.eventHandlers.has(event)) {
|
|
409
|
+
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
410
|
+
}
|
|
411
|
+
this.eventHandlers.get(event).add(handler);
|
|
412
|
+
}
|
|
413
|
+
// ===============================
|
|
414
|
+
// Flag Resolution Methods - Delegated to OFREP Provider
|
|
415
|
+
// ===============================
|
|
416
|
+
resolveBooleanEvaluation(flagKey, defaultValue, context) {
|
|
417
|
+
return this.ofrepProvider.resolveBooleanEvaluation(flagKey, defaultValue, context);
|
|
418
|
+
}
|
|
419
|
+
resolveStringEvaluation(flagKey, defaultValue, context) {
|
|
420
|
+
return this.ofrepProvider.resolveStringEvaluation(flagKey, defaultValue, context);
|
|
421
|
+
}
|
|
422
|
+
resolveNumberEvaluation(flagKey, defaultValue, context) {
|
|
423
|
+
return this.ofrepProvider.resolveNumberEvaluation(flagKey, defaultValue, context);
|
|
424
|
+
}
|
|
425
|
+
resolveObjectEvaluation(flagKey, defaultValue, context) {
|
|
426
|
+
return this.ofrepProvider.resolveObjectEvaluation(flagKey, defaultValue, context);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get SSE connection status.
|
|
430
|
+
*/
|
|
431
|
+
getSseStatus() {
|
|
432
|
+
return this.sseClient?.getStatus() ?? "disconnected";
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Force reconnect SSE connection.
|
|
436
|
+
*/
|
|
437
|
+
reconnectSse() {
|
|
438
|
+
if (this.enableRealtime && this.sseClient) {
|
|
439
|
+
this.sseClient.close();
|
|
440
|
+
this.startSseConnection();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ===============================
|
|
444
|
+
// Bulk Flag Evaluation (Direct HTTP - OFREP providers don't expose bulk API)
|
|
445
|
+
// ===============================
|
|
446
|
+
/**
|
|
447
|
+
* Transform OpenFeature context to OFREP context format.
|
|
448
|
+
*/
|
|
449
|
+
transformContext(context) {
|
|
450
|
+
const result = {};
|
|
451
|
+
if (context.targetingKey) {
|
|
452
|
+
result.targetingKey = context.targetingKey;
|
|
453
|
+
}
|
|
454
|
+
for (const [key, value] of Object.entries(context)) {
|
|
455
|
+
if (key !== "targetingKey") {
|
|
456
|
+
result[key] = value;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Infer the type of a value.
|
|
463
|
+
*/
|
|
464
|
+
inferType(value) {
|
|
465
|
+
if (value === null) return "null";
|
|
466
|
+
if (Array.isArray(value)) return "array";
|
|
467
|
+
const t = typeof value;
|
|
468
|
+
if (t === "boolean" || t === "string" || t === "number" || t === "object") {
|
|
469
|
+
return t;
|
|
470
|
+
}
|
|
471
|
+
return "unknown";
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get flag type from metadata or infer from value.
|
|
475
|
+
*/
|
|
476
|
+
getFlagType(flag) {
|
|
477
|
+
if (flag.metadata?.flagType) {
|
|
478
|
+
const metaType = flag.metadata.flagType;
|
|
479
|
+
if (metaType === "boolean" || metaType === "string" || metaType === "integer" || metaType === "decimal") {
|
|
480
|
+
return metaType === "integer" || metaType === "decimal" ? "number" : metaType;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return this.inferType(flag.value);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Format a value for display.
|
|
487
|
+
*/
|
|
488
|
+
formatValue(value) {
|
|
489
|
+
if (value === null) return "null";
|
|
490
|
+
if (typeof value === "string") return `"${value}"`;
|
|
491
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
492
|
+
return String(value);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Evaluate all flags for the given context.
|
|
496
|
+
* Returns a list of all flag evaluations with their keys, values, types, and reasons.
|
|
497
|
+
*
|
|
498
|
+
* Note: This method makes direct HTTP calls since OFREP providers don't expose
|
|
499
|
+
* the bulk evaluation API.
|
|
500
|
+
*
|
|
501
|
+
* @param context The evaluation context
|
|
502
|
+
* @returns List of flag evaluations
|
|
503
|
+
*/
|
|
504
|
+
async evaluateAllFlags(context) {
|
|
505
|
+
try {
|
|
506
|
+
const response = await this.fetchImpl(`${this.baseUrl}/ofrep/v1/evaluate/flags`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: {
|
|
509
|
+
"Content-Type": "application/json",
|
|
510
|
+
"X-API-Key": this.apiKey,
|
|
511
|
+
...this.getTelemetryHeaders()
|
|
512
|
+
},
|
|
513
|
+
body: JSON.stringify({
|
|
514
|
+
context: this.transformContext(context)
|
|
515
|
+
})
|
|
516
|
+
});
|
|
517
|
+
if (!response.ok) {
|
|
518
|
+
console.error(`Failed to evaluate all flags: ${response.status}`);
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
const result = await response.json();
|
|
522
|
+
const flags = [];
|
|
523
|
+
if (result.flags && Array.isArray(result.flags)) {
|
|
524
|
+
for (const flag of result.flags) {
|
|
525
|
+
if (flag.key) {
|
|
526
|
+
flags.push({
|
|
527
|
+
key: flag.key,
|
|
528
|
+
value: flag.value,
|
|
529
|
+
valueType: this.getFlagType(flag),
|
|
530
|
+
reason: flag.reason ?? null,
|
|
531
|
+
variant: flag.variant ?? null
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return flags;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error("Error evaluating all flags:", error);
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Evaluate a single flag and return its evaluation result.
|
|
544
|
+
*
|
|
545
|
+
* Note: This method makes direct HTTP calls for demo purposes.
|
|
546
|
+
* For standard flag evaluation, use the OpenFeature client methods.
|
|
547
|
+
*
|
|
548
|
+
* @param flagKey The flag key to evaluate
|
|
549
|
+
* @param context The evaluation context
|
|
550
|
+
* @returns The flag evaluation, or null if the flag doesn't exist
|
|
551
|
+
*/
|
|
552
|
+
async evaluateFlag(flagKey, context) {
|
|
553
|
+
try {
|
|
554
|
+
const response = await this.fetchImpl(`${this.baseUrl}/ofrep/v1/evaluate/flags/${flagKey}`, {
|
|
555
|
+
method: "POST",
|
|
556
|
+
headers: {
|
|
557
|
+
"Content-Type": "application/json",
|
|
558
|
+
"X-API-Key": this.apiKey,
|
|
559
|
+
...this.getTelemetryHeaders()
|
|
560
|
+
},
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
context: this.transformContext(context)
|
|
563
|
+
})
|
|
564
|
+
});
|
|
565
|
+
if (!response.ok) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const result = await response.json();
|
|
569
|
+
return {
|
|
570
|
+
key: result.key ?? flagKey,
|
|
571
|
+
value: result.value,
|
|
572
|
+
valueType: this.getFlagType(result),
|
|
573
|
+
reason: result.reason ?? null,
|
|
574
|
+
variant: result.variant ?? null
|
|
575
|
+
};
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error(`Error evaluating flag '${flagKey}':`, error);
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// src/cache.ts
|
|
584
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
585
|
+
var FlagCache = class {
|
|
586
|
+
/**
|
|
587
|
+
* Create a new FlagCache.
|
|
588
|
+
* @param ttlMs Time-to-live in milliseconds (default: 5 minutes)
|
|
589
|
+
*/
|
|
590
|
+
constructor(ttlMs = DEFAULT_TTL_MS) {
|
|
591
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
592
|
+
this.ttlMs = ttlMs;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Get a value from the cache.
|
|
596
|
+
* Returns undefined if the key doesn't exist or has expired.
|
|
597
|
+
*/
|
|
598
|
+
get(key) {
|
|
599
|
+
const entry = this.cache.get(key);
|
|
600
|
+
if (!entry) {
|
|
601
|
+
return void 0;
|
|
602
|
+
}
|
|
603
|
+
if (Date.now() > entry.expiresAt) {
|
|
604
|
+
this.cache.delete(key);
|
|
605
|
+
return void 0;
|
|
606
|
+
}
|
|
607
|
+
return entry.value;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Set a value in the cache.
|
|
611
|
+
*/
|
|
612
|
+
set(key, value) {
|
|
613
|
+
this.cache.set(key, {
|
|
614
|
+
value,
|
|
615
|
+
expiresAt: Date.now() + this.ttlMs
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Invalidate a specific key or all keys if no key is provided.
|
|
620
|
+
*/
|
|
621
|
+
invalidate(key) {
|
|
622
|
+
if (key) {
|
|
623
|
+
this.cache.delete(key);
|
|
624
|
+
} else {
|
|
625
|
+
this.cache.clear();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Handle a flag change event from SSE.
|
|
630
|
+
* Invalidates the specific flag or all flags if flagKey is null.
|
|
631
|
+
*/
|
|
632
|
+
handleFlagChange(event) {
|
|
633
|
+
if (event.flagKey) {
|
|
634
|
+
this.invalidate(event.flagKey);
|
|
635
|
+
} else {
|
|
636
|
+
this.invalidate();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
641
|
+
0 && (module.exports = {
|
|
642
|
+
FlagCache,
|
|
643
|
+
FlipswitchProvider,
|
|
644
|
+
SseClient
|
|
645
|
+
});
|