@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.
@@ -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 };