@cloudflare/flagship 0.0.0 → 0.0.1
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 +184 -0
- package/README.md +5 -0
- package/dist/index-C_sW3e_7.d.mts +233 -0
- package/dist/index-D8YLMfBG.d.cts +233 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/server.cjs +298 -0
- package/dist/server.d.cts +144 -0
- package/dist/server.d.mts +144 -0
- package/dist/server.mjs +290 -0
- package/dist/src-CiVDWmng.mjs +202 -0
- package/dist/src-De-abNIr.cjs +231 -0
- package/dist/web.cjs +148 -0
- package/dist/web.d.cts +72 -0
- package/dist/web.d.mts +72 -0
- package/dist/web.mjs +142 -0
- package/package.json +102 -10
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
//#region src/types.ts
|
|
2
|
+
/** Default base URL for the Flagship API. */
|
|
3
|
+
const FLAGSHIP_DEFAULT_BASE_URL = "https://api.cloudflare.com";
|
|
4
|
+
/**
|
|
5
|
+
* Internal error codes produced by `FlagshipClient`.
|
|
6
|
+
* These are mapped to OpenFeature `ErrorCode` values by the providers.
|
|
7
|
+
*/
|
|
8
|
+
let FlagshipErrorCode = /* @__PURE__ */ function(FlagshipErrorCode) {
|
|
9
|
+
/** HTTP or fetch-level failure (non-404/400 status, connection refused, etc.) */
|
|
10
|
+
FlagshipErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
11
|
+
/** The request was aborted because the configured timeout elapsed. */
|
|
12
|
+
FlagshipErrorCode["TIMEOUT_ERROR"] = "TIMEOUT_ERROR";
|
|
13
|
+
/** The response body was not a valid evaluation response. */
|
|
14
|
+
FlagshipErrorCode["PARSE_ERROR"] = "PARSE_ERROR";
|
|
15
|
+
/** The evaluation context contained complex values that cannot be serialized to query parameters. */
|
|
16
|
+
FlagshipErrorCode["INVALID_CONTEXT"] = "INVALID_CONTEXT";
|
|
17
|
+
return FlagshipErrorCode;
|
|
18
|
+
}({});
|
|
19
|
+
/**
|
|
20
|
+
* Error thrown by `FlagshipClient` for all abnormal conditions.
|
|
21
|
+
* Carries a `code` for programmatic handling and an optional `cause` which
|
|
22
|
+
* is the underlying `Response` object for HTTP errors, allowing callers to
|
|
23
|
+
* inspect the status code (e.g. to distinguish 404 → `FLAG_NOT_FOUND`).
|
|
24
|
+
*/
|
|
25
|
+
var FlagshipError = class extends Error {
|
|
26
|
+
constructor(message, code, cause) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.cause = cause;
|
|
30
|
+
this.name = "FlagshipError";
|
|
31
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/context.ts
|
|
36
|
+
/**
|
|
37
|
+
* Utility for transforming OpenFeature evaluation context
|
|
38
|
+
*/
|
|
39
|
+
var ContextTransformer = class {
|
|
40
|
+
/**
|
|
41
|
+
* Transform OpenFeature evaluation context to query parameters
|
|
42
|
+
* for the Flagship API.
|
|
43
|
+
*
|
|
44
|
+
* Primitive values (`string`, `number`, `boolean`) and `Date` objects are
|
|
45
|
+
* serialized directly. Nested objects and arrays cannot be expressed as query
|
|
46
|
+
* parameters and are skipped.
|
|
47
|
+
*
|
|
48
|
+
* When a `droppedKeys` collector array is provided, skipped key names are
|
|
49
|
+
* pushed into it and **no** console warning is emitted — the caller is
|
|
50
|
+
* expected to handle the situation (e.g. throw `INVALID_CONTEXT`).
|
|
51
|
+
* When no collector is provided, a `console.warn` is emitted for each
|
|
52
|
+
* skipped key so the issue is still surfaced in development.
|
|
53
|
+
*
|
|
54
|
+
* @param context - OpenFeature evaluation context
|
|
55
|
+
* @param droppedKeys - Optional collector array; skipped key names are pushed here
|
|
56
|
+
*/
|
|
57
|
+
static toQueryParams(context, droppedKeys) {
|
|
58
|
+
const params = {};
|
|
59
|
+
for (const [key, value] of Object.entries(context)) {
|
|
60
|
+
if (value === void 0 || value === null) continue;
|
|
61
|
+
if (value instanceof Date) {
|
|
62
|
+
params[key] = value.toISOString();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
66
|
+
params[key] = String(value);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "object") {
|
|
70
|
+
if (droppedKeys) droppedKeys.push(key);
|
|
71
|
+
else console.warn(`[Flagship] Context key "${key}" is a complex object/array and cannot be serialized to a query parameter. This value will be ignored during flag evaluation.`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return params;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build URL with query parameters from context.
|
|
79
|
+
*
|
|
80
|
+
* @param baseUrl - The base evaluation endpoint URL
|
|
81
|
+
* @param flagKey - The flag key to evaluate
|
|
82
|
+
* @param context - OpenFeature evaluation context
|
|
83
|
+
* @param droppedKeys - Optional collector array; skipped context key names are pushed here
|
|
84
|
+
*/
|
|
85
|
+
static buildUrl(baseUrl, flagKey, context, droppedKeys) {
|
|
86
|
+
const url = new URL(baseUrl);
|
|
87
|
+
url.searchParams.set("flagKey", flagKey);
|
|
88
|
+
const params = this.toQueryParams(context, droppedKeys);
|
|
89
|
+
for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
|
|
90
|
+
return url.toString();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/client.ts
|
|
95
|
+
var FlagshipClient = class {
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.options = {
|
|
98
|
+
endpoint: resolveEndpoint(options),
|
|
99
|
+
fetchOptions: buildFetchOptions(options),
|
|
100
|
+
timeout: options.timeout || 5e3,
|
|
101
|
+
retries: Math.min(options.retries !== void 0 ? options.retries : 1, 10),
|
|
102
|
+
retryDelay: Math.min(options.retryDelay !== void 0 ? options.retryDelay : 1e3, 3e4)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Evaluate a flag with the given context.
|
|
107
|
+
*
|
|
108
|
+
* Throws a `FlagshipError` with `FlagshipErrorCode.INVALID_CONTEXT` if the
|
|
109
|
+
* evaluation context contains complex values (objects or arrays) that cannot
|
|
110
|
+
* be serialized to query parameters.
|
|
111
|
+
*/
|
|
112
|
+
async evaluate(flagKey, context) {
|
|
113
|
+
const droppedKeys = [];
|
|
114
|
+
const url = ContextTransformer.buildUrl(this.options.endpoint, flagKey, context, droppedKeys);
|
|
115
|
+
if (droppedKeys.length > 0) throw new FlagshipError(`Evaluation context contains complex values that cannot be serialized for flag "${flagKey}". Unsupported keys: ${droppedKeys.join(", ")}. Use primitive values (string, number, boolean) or Date objects.`, FlagshipErrorCode.INVALID_CONTEXT);
|
|
116
|
+
return this.fetchWithRetry(url, this.options.retries);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fetch with retry logic. Only retries on transient network/server errors —
|
|
120
|
+
* 404 and 400 responses are terminal and propagated immediately.
|
|
121
|
+
*/
|
|
122
|
+
async fetchWithRetry(url, retriesLeft) {
|
|
123
|
+
try {
|
|
124
|
+
return await this.fetchWithTimeout(url, this.options.timeout);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof FlagshipError && error.cause instanceof Response) {
|
|
127
|
+
const status = error.cause.status;
|
|
128
|
+
if (status === 404 || status === 400) throw error;
|
|
129
|
+
}
|
|
130
|
+
if (retriesLeft > 0) {
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, this.options.retryDelay));
|
|
132
|
+
return this.fetchWithRetry(url, retriesLeft - 1);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Fetch with timeout using AbortController
|
|
139
|
+
*/
|
|
140
|
+
async fetchWithTimeout(url, timeout) {
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(url, {
|
|
145
|
+
...this.options.fetchOptions,
|
|
146
|
+
signal: controller.signal
|
|
147
|
+
});
|
|
148
|
+
clearTimeout(timeoutId);
|
|
149
|
+
if (!response.ok) throw new FlagshipError(`HTTP ${response.status}: ${response.statusText}`, FlagshipErrorCode.NETWORK_ERROR, response);
|
|
150
|
+
const data = await response.json();
|
|
151
|
+
if (!data || typeof data !== "object" || !("flagKey" in data) || !("value" in data)) throw new FlagshipError("Invalid response format from Flagship API", FlagshipErrorCode.PARSE_ERROR);
|
|
152
|
+
return data;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
if (error instanceof Error && error.name === "AbortError") throw new FlagshipError(`Request timeout after ${timeout}ms`, FlagshipErrorCode.TIMEOUT_ERROR, error);
|
|
156
|
+
if (error instanceof FlagshipError) throw error;
|
|
157
|
+
throw new FlagshipError(`Network error: ${error}`, FlagshipErrorCode.NETWORK_ERROR, error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Merge `authToken` and `fetchOptions` into a single `RequestInit`.
|
|
163
|
+
*
|
|
164
|
+
* Precedence for the `Authorization` header (highest → lowest):
|
|
165
|
+
* 1. An explicit `Authorization` value inside `fetchOptions.headers`
|
|
166
|
+
* 2. A value derived from `authToken`
|
|
167
|
+
*
|
|
168
|
+
* All other `fetchOptions` fields are spread as-is.
|
|
169
|
+
*/
|
|
170
|
+
function buildFetchOptions(options) {
|
|
171
|
+
const { authToken, fetchOptions = {} } = options;
|
|
172
|
+
if (!authToken) return fetchOptions;
|
|
173
|
+
const existingHeaders = new Headers(fetchOptions.headers);
|
|
174
|
+
if (!existingHeaders.has("Authorization")) existingHeaders.set("Authorization", `Bearer ${authToken}`);
|
|
175
|
+
return {
|
|
176
|
+
...fetchOptions,
|
|
177
|
+
headers: existingHeaders
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function resolveEndpoint(options) {
|
|
181
|
+
const { appId, endpoint, baseUrl, accountId } = options;
|
|
182
|
+
if (appId && endpoint) throw new Error("Flagship: provide either \"appId\" or \"endpoint\", not both");
|
|
183
|
+
if (!appId && !endpoint) throw new Error("Flagship: either \"appId\" or \"endpoint\" is required");
|
|
184
|
+
if (endpoint) {
|
|
185
|
+
try {
|
|
186
|
+
new URL(endpoint);
|
|
187
|
+
} catch {
|
|
188
|
+
throw new Error(`Flagship: invalid endpoint URL: ${endpoint}`);
|
|
189
|
+
}
|
|
190
|
+
return endpoint;
|
|
191
|
+
}
|
|
192
|
+
if (!accountId) throw new Error("Flagship: \"accountId\" is required when using \"appId\"");
|
|
193
|
+
const resolved = `${(baseUrl || "https://api.cloudflare.com").replace(/\/+$/, "")}/client/v4/accounts/${encodeURIComponent(accountId)}/flagship/apps/${encodeURIComponent(appId)}/evaluate`;
|
|
194
|
+
try {
|
|
195
|
+
new URL(resolved);
|
|
196
|
+
} catch {
|
|
197
|
+
throw new Error(`Flagship: resolved endpoint is not a valid URL: ${resolved}`);
|
|
198
|
+
}
|
|
199
|
+
return resolved;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
Object.defineProperty(exports, "ContextTransformer", {
|
|
203
|
+
enumerable: true,
|
|
204
|
+
get: function() {
|
|
205
|
+
return ContextTransformer;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
Object.defineProperty(exports, "FLAGSHIP_DEFAULT_BASE_URL", {
|
|
209
|
+
enumerable: true,
|
|
210
|
+
get: function() {
|
|
211
|
+
return FLAGSHIP_DEFAULT_BASE_URL;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
Object.defineProperty(exports, "FlagshipClient", {
|
|
215
|
+
enumerable: true,
|
|
216
|
+
get: function() {
|
|
217
|
+
return FlagshipClient;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
Object.defineProperty(exports, "FlagshipError", {
|
|
221
|
+
enumerable: true,
|
|
222
|
+
get: function() {
|
|
223
|
+
return FlagshipError;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
Object.defineProperty(exports, "FlagshipErrorCode", {
|
|
227
|
+
enumerable: true,
|
|
228
|
+
get: function() {
|
|
229
|
+
return FlagshipErrorCode;
|
|
230
|
+
}
|
|
231
|
+
});
|
package/dist/web.cjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_src = require("./src-De-abNIr.cjs");
|
|
3
|
+
let _openfeature_web_sdk = require("@openfeature/web-sdk");
|
|
4
|
+
//#region src/client-provider.ts
|
|
5
|
+
/**
|
|
6
|
+
* OpenFeature provider for Flagship (client-side / browser).
|
|
7
|
+
*
|
|
8
|
+
* Fetches all flags listed in `prefetchFlags` during initialization and on
|
|
9
|
+
* every context change, storing results in an in-memory cache. All
|
|
10
|
+
* `resolve*` methods are synchronous, as required by the OpenFeature web SDK.
|
|
11
|
+
*
|
|
12
|
+
* A cache miss (flag key not in `prefetchFlags`, or fetch failed) returns
|
|
13
|
+
* `ErrorCode.FLAG_NOT_FOUND` with the default value.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { OpenFeature } from '@openfeature/web-sdk';
|
|
18
|
+
* import { FlagshipClientProvider } from '@cloudflare/flagship/web';
|
|
19
|
+
*
|
|
20
|
+
* await OpenFeature.setProviderAndWait(
|
|
21
|
+
* new FlagshipClientProvider({
|
|
22
|
+
* appId: 'app-abc123',
|
|
23
|
+
* accountId: 'your-account-id',
|
|
24
|
+
* authToken: 'your-token',
|
|
25
|
+
* prefetchFlags: ['dark-mode', 'welcome-message'],
|
|
26
|
+
* })
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'premium' });
|
|
30
|
+
*
|
|
31
|
+
* const client = OpenFeature.getClient();
|
|
32
|
+
* const darkMode = client.getBooleanValue('dark-mode', false);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
var FlagshipClientProvider = class {
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.runsOn = "client";
|
|
38
|
+
this.events = new _openfeature_web_sdk.OpenFeatureEventEmitter();
|
|
39
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
40
|
+
this.currentStatus = _openfeature_web_sdk.ProviderStatus.NOT_READY;
|
|
41
|
+
this.metadata = { name: "Flagship Client Provider" };
|
|
42
|
+
this.client = new require_src.FlagshipClient(options);
|
|
43
|
+
this.prefetchFlags = options.prefetchFlags || [];
|
|
44
|
+
this.logging = options.logging ?? false;
|
|
45
|
+
}
|
|
46
|
+
get status() {
|
|
47
|
+
return this.currentStatus;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fetches all `prefetchFlags` in parallel and populates the cache.
|
|
51
|
+
* Individual flag fetch failures are logged when `logging` is enabled but
|
|
52
|
+
* do not prevent the provider from reaching READY.
|
|
53
|
+
*/
|
|
54
|
+
async initialize(context = {}) {
|
|
55
|
+
await this.fetchAll(context, "initialization");
|
|
56
|
+
this.currentStatus = _openfeature_web_sdk.ProviderStatus.READY;
|
|
57
|
+
this.events.emit(_openfeature_web_sdk.ProviderEvents.Ready);
|
|
58
|
+
}
|
|
59
|
+
async onClose() {
|
|
60
|
+
this.cache.clear();
|
|
61
|
+
this.currentStatus = _openfeature_web_sdk.ProviderStatus.NOT_READY;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Invalidates the entire cache and re-fetches all `prefetchFlags` for the
|
|
65
|
+
* new context. Returning a Promise causes the SDK to automatically emit
|
|
66
|
+
* `ProviderEvents.Reconciling` while this method executes.
|
|
67
|
+
*/
|
|
68
|
+
async onContextChange(_oldContext, newContext = {}) {
|
|
69
|
+
this.cache.clear();
|
|
70
|
+
await this.fetchAll(newContext, "context change");
|
|
71
|
+
}
|
|
72
|
+
resolveBooleanEvaluation(flagKey, defaultValue, _context, logger) {
|
|
73
|
+
return this.resolveFromCache(flagKey, defaultValue, "boolean", logger);
|
|
74
|
+
}
|
|
75
|
+
resolveStringEvaluation(flagKey, defaultValue, _context, logger) {
|
|
76
|
+
return this.resolveFromCache(flagKey, defaultValue, "string", logger);
|
|
77
|
+
}
|
|
78
|
+
resolveNumberEvaluation(flagKey, defaultValue, _context, logger) {
|
|
79
|
+
return this.resolveFromCache(flagKey, defaultValue, "number", logger);
|
|
80
|
+
}
|
|
81
|
+
resolveObjectEvaluation(flagKey, defaultValue, _context, logger) {
|
|
82
|
+
return this.resolveFromCache(flagKey, defaultValue, "object", logger);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fetches all `prefetchFlags` in parallel using `Promise.allSettled`.
|
|
86
|
+
* Failures are logged individually when `logging` is enabled.
|
|
87
|
+
*/
|
|
88
|
+
async fetchAll(context, phase) {
|
|
89
|
+
if (this.prefetchFlags.length === 0) return;
|
|
90
|
+
const results = await Promise.allSettled(this.prefetchFlags.map(async (flagKey) => {
|
|
91
|
+
const result = await this.client.evaluate(flagKey, context);
|
|
92
|
+
this.cache.set(flagKey, {
|
|
93
|
+
value: result.value,
|
|
94
|
+
reason: result.reason,
|
|
95
|
+
variant: result.variant
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
98
|
+
if (this.logging) results.forEach((result, i) => {
|
|
99
|
+
if (result.status === "rejected") {
|
|
100
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
101
|
+
console.warn(`[Flagship] Failed to fetch flag "${this.prefetchFlags[i]}" during ${phase}: ${reason}`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
resolveFromCache(flagKey, defaultValue, expectedType, logger) {
|
|
106
|
+
const cached = this.cache.get(flagKey);
|
|
107
|
+
if (!cached) {
|
|
108
|
+
const msg = `Flag "${flagKey}" not found in cache. Add it to prefetchFlags to ensure it is fetched on initialization.`;
|
|
109
|
+
if (this.logging) logger.warn(`[Flagship] ${msg}`);
|
|
110
|
+
return {
|
|
111
|
+
value: defaultValue,
|
|
112
|
+
reason: "ERROR",
|
|
113
|
+
errorCode: _openfeature_web_sdk.ErrorCode.FLAG_NOT_FOUND,
|
|
114
|
+
errorMessage: msg
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const actualType = this.getValueType(cached.value);
|
|
118
|
+
if (actualType !== expectedType) {
|
|
119
|
+
const msg = `Flag "${flagKey}" type mismatch: expected ${expectedType}, got ${actualType}`;
|
|
120
|
+
if (this.logging) logger.warn(`[Flagship] ${msg}`);
|
|
121
|
+
return {
|
|
122
|
+
value: defaultValue,
|
|
123
|
+
errorCode: _openfeature_web_sdk.ErrorCode.TYPE_MISMATCH,
|
|
124
|
+
errorMessage: msg,
|
|
125
|
+
reason: "ERROR"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
value: cached.value,
|
|
130
|
+
reason: "CACHED",
|
|
131
|
+
variant: cached.variant,
|
|
132
|
+
flagMetadata: {}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
getValueType(value) {
|
|
136
|
+
if (typeof value === "boolean") return "boolean";
|
|
137
|
+
if (typeof value === "string") return "string";
|
|
138
|
+
if (typeof value === "number") return "number";
|
|
139
|
+
return "object";
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
exports.ContextTransformer = require_src.ContextTransformer;
|
|
144
|
+
exports.FLAGSHIP_DEFAULT_BASE_URL = require_src.FLAGSHIP_DEFAULT_BASE_URL;
|
|
145
|
+
exports.FlagshipClient = require_src.FlagshipClient;
|
|
146
|
+
exports.FlagshipClientProvider = FlagshipClientProvider;
|
|
147
|
+
exports.FlagshipError = require_src.FlagshipError;
|
|
148
|
+
exports.FlagshipErrorCode = require_src.FlagshipErrorCode;
|
package/dist/web.d.cts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { a as FlagshipClientProviderOptions, c as FlagshipEvaluationResponse, i as FLAGSHIP_DEFAULT_BASE_URL, l as FlagshipProviderOptions, n as ContextTransformer, o as FlagshipError, r as CachedFlag, s as FlagshipErrorCode, t as FlagshipClient } from "./index-D8YLMfBG.cjs";
|
|
2
|
+
import { EvaluationContext, JsonValue, Logger, OpenFeatureEventEmitter, Provider, ProviderMetadata, ProviderStatus, ResolutionDetails } from "@openfeature/web-sdk";
|
|
3
|
+
|
|
4
|
+
//#region src/client-provider.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* OpenFeature provider for Flagship (client-side / browser).
|
|
7
|
+
*
|
|
8
|
+
* Fetches all flags listed in `prefetchFlags` during initialization and on
|
|
9
|
+
* every context change, storing results in an in-memory cache. All
|
|
10
|
+
* `resolve*` methods are synchronous, as required by the OpenFeature web SDK.
|
|
11
|
+
*
|
|
12
|
+
* A cache miss (flag key not in `prefetchFlags`, or fetch failed) returns
|
|
13
|
+
* `ErrorCode.FLAG_NOT_FOUND` with the default value.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { OpenFeature } from '@openfeature/web-sdk';
|
|
18
|
+
* import { FlagshipClientProvider } from '@cloudflare/flagship/web';
|
|
19
|
+
*
|
|
20
|
+
* await OpenFeature.setProviderAndWait(
|
|
21
|
+
* new FlagshipClientProvider({
|
|
22
|
+
* appId: 'app-abc123',
|
|
23
|
+
* accountId: 'your-account-id',
|
|
24
|
+
* authToken: 'your-token',
|
|
25
|
+
* prefetchFlags: ['dark-mode', 'welcome-message'],
|
|
26
|
+
* })
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'premium' });
|
|
30
|
+
*
|
|
31
|
+
* const client = OpenFeature.getClient();
|
|
32
|
+
* const darkMode = client.getBooleanValue('dark-mode', false);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare class FlagshipClientProvider implements Provider {
|
|
36
|
+
readonly metadata: ProviderMetadata;
|
|
37
|
+
readonly runsOn: "client";
|
|
38
|
+
readonly events: OpenFeatureEventEmitter;
|
|
39
|
+
private cache;
|
|
40
|
+
private client;
|
|
41
|
+
private readonly prefetchFlags;
|
|
42
|
+
private readonly logging;
|
|
43
|
+
private currentStatus;
|
|
44
|
+
constructor(options: FlagshipClientProviderOptions);
|
|
45
|
+
get status(): ProviderStatus;
|
|
46
|
+
/**
|
|
47
|
+
* Fetches all `prefetchFlags` in parallel and populates the cache.
|
|
48
|
+
* Individual flag fetch failures are logged when `logging` is enabled but
|
|
49
|
+
* do not prevent the provider from reaching READY.
|
|
50
|
+
*/
|
|
51
|
+
initialize(context?: EvaluationContext): Promise<void>;
|
|
52
|
+
onClose(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Invalidates the entire cache and re-fetches all `prefetchFlags` for the
|
|
55
|
+
* new context. Returning a Promise causes the SDK to automatically emit
|
|
56
|
+
* `ProviderEvents.Reconciling` while this method executes.
|
|
57
|
+
*/
|
|
58
|
+
onContextChange(_oldContext: EvaluationContext, newContext?: EvaluationContext): Promise<void>;
|
|
59
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, _context: EvaluationContext, logger: Logger): ResolutionDetails<boolean>;
|
|
60
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext, logger: Logger): ResolutionDetails<string>;
|
|
61
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext, logger: Logger): ResolutionDetails<number>;
|
|
62
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext, logger: Logger): ResolutionDetails<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Fetches all `prefetchFlags` in parallel using `Promise.allSettled`.
|
|
65
|
+
* Failures are logged individually when `logging` is enabled.
|
|
66
|
+
*/
|
|
67
|
+
private fetchAll;
|
|
68
|
+
private resolveFromCache;
|
|
69
|
+
private getValueType;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { type CachedFlag, ContextTransformer, FLAGSHIP_DEFAULT_BASE_URL, FlagshipClient, FlagshipClientProvider, type FlagshipClientProviderOptions, FlagshipError, FlagshipErrorCode, type FlagshipEvaluationResponse, type FlagshipProviderOptions };
|
package/dist/web.d.mts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { a as FlagshipClientProviderOptions, c as FlagshipEvaluationResponse, i as FLAGSHIP_DEFAULT_BASE_URL, l as FlagshipProviderOptions, n as ContextTransformer, o as FlagshipError, r as CachedFlag, s as FlagshipErrorCode, t as FlagshipClient } from "./index-C_sW3e_7.mjs";
|
|
2
|
+
import { EvaluationContext, JsonValue, Logger, OpenFeatureEventEmitter, Provider, ProviderMetadata, ProviderStatus, ResolutionDetails } from "@openfeature/web-sdk";
|
|
3
|
+
|
|
4
|
+
//#region src/client-provider.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* OpenFeature provider for Flagship (client-side / browser).
|
|
7
|
+
*
|
|
8
|
+
* Fetches all flags listed in `prefetchFlags` during initialization and on
|
|
9
|
+
* every context change, storing results in an in-memory cache. All
|
|
10
|
+
* `resolve*` methods are synchronous, as required by the OpenFeature web SDK.
|
|
11
|
+
*
|
|
12
|
+
* A cache miss (flag key not in `prefetchFlags`, or fetch failed) returns
|
|
13
|
+
* `ErrorCode.FLAG_NOT_FOUND` with the default value.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { OpenFeature } from '@openfeature/web-sdk';
|
|
18
|
+
* import { FlagshipClientProvider } from '@cloudflare/flagship/web';
|
|
19
|
+
*
|
|
20
|
+
* await OpenFeature.setProviderAndWait(
|
|
21
|
+
* new FlagshipClientProvider({
|
|
22
|
+
* appId: 'app-abc123',
|
|
23
|
+
* accountId: 'your-account-id',
|
|
24
|
+
* authToken: 'your-token',
|
|
25
|
+
* prefetchFlags: ['dark-mode', 'welcome-message'],
|
|
26
|
+
* })
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'premium' });
|
|
30
|
+
*
|
|
31
|
+
* const client = OpenFeature.getClient();
|
|
32
|
+
* const darkMode = client.getBooleanValue('dark-mode', false);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare class FlagshipClientProvider implements Provider {
|
|
36
|
+
readonly metadata: ProviderMetadata;
|
|
37
|
+
readonly runsOn: "client";
|
|
38
|
+
readonly events: OpenFeatureEventEmitter;
|
|
39
|
+
private cache;
|
|
40
|
+
private client;
|
|
41
|
+
private readonly prefetchFlags;
|
|
42
|
+
private readonly logging;
|
|
43
|
+
private currentStatus;
|
|
44
|
+
constructor(options: FlagshipClientProviderOptions);
|
|
45
|
+
get status(): ProviderStatus;
|
|
46
|
+
/**
|
|
47
|
+
* Fetches all `prefetchFlags` in parallel and populates the cache.
|
|
48
|
+
* Individual flag fetch failures are logged when `logging` is enabled but
|
|
49
|
+
* do not prevent the provider from reaching READY.
|
|
50
|
+
*/
|
|
51
|
+
initialize(context?: EvaluationContext): Promise<void>;
|
|
52
|
+
onClose(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Invalidates the entire cache and re-fetches all `prefetchFlags` for the
|
|
55
|
+
* new context. Returning a Promise causes the SDK to automatically emit
|
|
56
|
+
* `ProviderEvents.Reconciling` while this method executes.
|
|
57
|
+
*/
|
|
58
|
+
onContextChange(_oldContext: EvaluationContext, newContext?: EvaluationContext): Promise<void>;
|
|
59
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, _context: EvaluationContext, logger: Logger): ResolutionDetails<boolean>;
|
|
60
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, _context: EvaluationContext, logger: Logger): ResolutionDetails<string>;
|
|
61
|
+
resolveNumberEvaluation(flagKey: string, defaultValue: number, _context: EvaluationContext, logger: Logger): ResolutionDetails<number>;
|
|
62
|
+
resolveObjectEvaluation<T extends JsonValue>(flagKey: string, defaultValue: T, _context: EvaluationContext, logger: Logger): ResolutionDetails<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Fetches all `prefetchFlags` in parallel using `Promise.allSettled`.
|
|
65
|
+
* Failures are logged individually when `logging` is enabled.
|
|
66
|
+
*/
|
|
67
|
+
private fetchAll;
|
|
68
|
+
private resolveFromCache;
|
|
69
|
+
private getValueType;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { type CachedFlag, ContextTransformer, FLAGSHIP_DEFAULT_BASE_URL, FlagshipClient, FlagshipClientProvider, type FlagshipClientProviderOptions, FlagshipError, FlagshipErrorCode, type FlagshipEvaluationResponse, type FlagshipProviderOptions };
|
package/dist/web.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { a as FlagshipErrorCode, i as FlagshipError, n as ContextTransformer, r as FLAGSHIP_DEFAULT_BASE_URL, t as FlagshipClient } from "./src-CiVDWmng.mjs";
|
|
2
|
+
import { ErrorCode, OpenFeatureEventEmitter, ProviderEvents, ProviderStatus } from "@openfeature/web-sdk";
|
|
3
|
+
//#region src/client-provider.ts
|
|
4
|
+
/**
|
|
5
|
+
* OpenFeature provider for Flagship (client-side / browser).
|
|
6
|
+
*
|
|
7
|
+
* Fetches all flags listed in `prefetchFlags` during initialization and on
|
|
8
|
+
* every context change, storing results in an in-memory cache. All
|
|
9
|
+
* `resolve*` methods are synchronous, as required by the OpenFeature web SDK.
|
|
10
|
+
*
|
|
11
|
+
* A cache miss (flag key not in `prefetchFlags`, or fetch failed) returns
|
|
12
|
+
* `ErrorCode.FLAG_NOT_FOUND` with the default value.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { OpenFeature } from '@openfeature/web-sdk';
|
|
17
|
+
* import { FlagshipClientProvider } from '@cloudflare/flagship/web';
|
|
18
|
+
*
|
|
19
|
+
* await OpenFeature.setProviderAndWait(
|
|
20
|
+
* new FlagshipClientProvider({
|
|
21
|
+
* appId: 'app-abc123',
|
|
22
|
+
* accountId: 'your-account-id',
|
|
23
|
+
* authToken: 'your-token',
|
|
24
|
+
* prefetchFlags: ['dark-mode', 'welcome-message'],
|
|
25
|
+
* })
|
|
26
|
+
* );
|
|
27
|
+
*
|
|
28
|
+
* await OpenFeature.setContext({ targetingKey: 'user-123', plan: 'premium' });
|
|
29
|
+
*
|
|
30
|
+
* const client = OpenFeature.getClient();
|
|
31
|
+
* const darkMode = client.getBooleanValue('dark-mode', false);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
var FlagshipClientProvider = class {
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.runsOn = "client";
|
|
37
|
+
this.events = new OpenFeatureEventEmitter();
|
|
38
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
39
|
+
this.currentStatus = ProviderStatus.NOT_READY;
|
|
40
|
+
this.metadata = { name: "Flagship Client Provider" };
|
|
41
|
+
this.client = new FlagshipClient(options);
|
|
42
|
+
this.prefetchFlags = options.prefetchFlags || [];
|
|
43
|
+
this.logging = options.logging ?? false;
|
|
44
|
+
}
|
|
45
|
+
get status() {
|
|
46
|
+
return this.currentStatus;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fetches all `prefetchFlags` in parallel and populates the cache.
|
|
50
|
+
* Individual flag fetch failures are logged when `logging` is enabled but
|
|
51
|
+
* do not prevent the provider from reaching READY.
|
|
52
|
+
*/
|
|
53
|
+
async initialize(context = {}) {
|
|
54
|
+
await this.fetchAll(context, "initialization");
|
|
55
|
+
this.currentStatus = ProviderStatus.READY;
|
|
56
|
+
this.events.emit(ProviderEvents.Ready);
|
|
57
|
+
}
|
|
58
|
+
async onClose() {
|
|
59
|
+
this.cache.clear();
|
|
60
|
+
this.currentStatus = ProviderStatus.NOT_READY;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Invalidates the entire cache and re-fetches all `prefetchFlags` for the
|
|
64
|
+
* new context. Returning a Promise causes the SDK to automatically emit
|
|
65
|
+
* `ProviderEvents.Reconciling` while this method executes.
|
|
66
|
+
*/
|
|
67
|
+
async onContextChange(_oldContext, newContext = {}) {
|
|
68
|
+
this.cache.clear();
|
|
69
|
+
await this.fetchAll(newContext, "context change");
|
|
70
|
+
}
|
|
71
|
+
resolveBooleanEvaluation(flagKey, defaultValue, _context, logger) {
|
|
72
|
+
return this.resolveFromCache(flagKey, defaultValue, "boolean", logger);
|
|
73
|
+
}
|
|
74
|
+
resolveStringEvaluation(flagKey, defaultValue, _context, logger) {
|
|
75
|
+
return this.resolveFromCache(flagKey, defaultValue, "string", logger);
|
|
76
|
+
}
|
|
77
|
+
resolveNumberEvaluation(flagKey, defaultValue, _context, logger) {
|
|
78
|
+
return this.resolveFromCache(flagKey, defaultValue, "number", logger);
|
|
79
|
+
}
|
|
80
|
+
resolveObjectEvaluation(flagKey, defaultValue, _context, logger) {
|
|
81
|
+
return this.resolveFromCache(flagKey, defaultValue, "object", logger);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetches all `prefetchFlags` in parallel using `Promise.allSettled`.
|
|
85
|
+
* Failures are logged individually when `logging` is enabled.
|
|
86
|
+
*/
|
|
87
|
+
async fetchAll(context, phase) {
|
|
88
|
+
if (this.prefetchFlags.length === 0) return;
|
|
89
|
+
const results = await Promise.allSettled(this.prefetchFlags.map(async (flagKey) => {
|
|
90
|
+
const result = await this.client.evaluate(flagKey, context);
|
|
91
|
+
this.cache.set(flagKey, {
|
|
92
|
+
value: result.value,
|
|
93
|
+
reason: result.reason,
|
|
94
|
+
variant: result.variant
|
|
95
|
+
});
|
|
96
|
+
}));
|
|
97
|
+
if (this.logging) results.forEach((result, i) => {
|
|
98
|
+
if (result.status === "rejected") {
|
|
99
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
100
|
+
console.warn(`[Flagship] Failed to fetch flag "${this.prefetchFlags[i]}" during ${phase}: ${reason}`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
resolveFromCache(flagKey, defaultValue, expectedType, logger) {
|
|
105
|
+
const cached = this.cache.get(flagKey);
|
|
106
|
+
if (!cached) {
|
|
107
|
+
const msg = `Flag "${flagKey}" not found in cache. Add it to prefetchFlags to ensure it is fetched on initialization.`;
|
|
108
|
+
if (this.logging) logger.warn(`[Flagship] ${msg}`);
|
|
109
|
+
return {
|
|
110
|
+
value: defaultValue,
|
|
111
|
+
reason: "ERROR",
|
|
112
|
+
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
|
113
|
+
errorMessage: msg
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const actualType = this.getValueType(cached.value);
|
|
117
|
+
if (actualType !== expectedType) {
|
|
118
|
+
const msg = `Flag "${flagKey}" type mismatch: expected ${expectedType}, got ${actualType}`;
|
|
119
|
+
if (this.logging) logger.warn(`[Flagship] ${msg}`);
|
|
120
|
+
return {
|
|
121
|
+
value: defaultValue,
|
|
122
|
+
errorCode: ErrorCode.TYPE_MISMATCH,
|
|
123
|
+
errorMessage: msg,
|
|
124
|
+
reason: "ERROR"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
value: cached.value,
|
|
129
|
+
reason: "CACHED",
|
|
130
|
+
variant: cached.variant,
|
|
131
|
+
flagMetadata: {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
getValueType(value) {
|
|
135
|
+
if (typeof value === "boolean") return "boolean";
|
|
136
|
+
if (typeof value === "string") return "string";
|
|
137
|
+
if (typeof value === "number") return "number";
|
|
138
|
+
return "object";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
export { ContextTransformer, FLAGSHIP_DEFAULT_BASE_URL, FlagshipClient, FlagshipClientProvider, FlagshipError, FlagshipErrorCode };
|