@spotify-confidence/openfeature-server-provider-local 0.6.0 → 0.8.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/CHANGELOG.md +29 -0
- package/README.md +89 -15
- package/dist/client.d.ts +129 -0
- package/dist/client.js +193 -0
- package/dist/confidence_resolver.wasm +0 -0
- package/dist/index.fetch.d.ts +528 -0
- package/dist/{index.browser.js → index.fetch.js} +413 -138
- package/dist/{index.browser.d.ts → index.inlined.d.ts} +75 -11
- package/dist/index.inlined.js +3225 -0
- package/dist/index.node.d.ts +80 -11
- package/dist/index.node.js +417 -142
- package/dist/server.d.ts +126 -0
- package/dist/server.js +113 -0
- package/package.json +37 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.7.0...openfeature-provider-js-v0.8.0) (2026-01-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **js:** add React support with useFlag and useFlagDetails hooks ([#246](https://github.com/spotify/confidence-resolver/issues/246)) ([d579a4c](https://github.com/spotify/confidence-resolver/commit/d579a4c8fe493ee3a92539203f21d1c03758c58e))
|
|
9
|
+
* **wasm:** add wasm API to apply previously resolved flags ([#235](https://github.com/spotify/confidence-resolver/issues/235)) ([79048f6](https://github.com/spotify/confidence-resolver/commit/79048f63a8c771eb98ecf478cab0b654aa745374))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Dependencies
|
|
13
|
+
|
|
14
|
+
* The following workspace dependencies were updated
|
|
15
|
+
* dependencies
|
|
16
|
+
* rust-guest bumped from 0.1.13 to 0.1.14
|
|
17
|
+
|
|
18
|
+
## [0.7.0](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.6.0...openfeature-provider-js-v0.7.0) (2026-01-22)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* inlined WASM for improved portability ([#243](https://github.com/spotify/confidence-resolver/issues/243)) ([8d86283](https://github.com/spotify/confidence-resolver/commit/8d862837244ffd9099bfeb56b42e203212e73a96))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Dependencies
|
|
27
|
+
|
|
28
|
+
* The following workspace dependencies were updated
|
|
29
|
+
* dependencies
|
|
30
|
+
* rust-guest bumped from 0.1.12 to 0.1.13
|
|
31
|
+
|
|
3
32
|
## [0.6.0](https://github.com/spotify/confidence-resolver/compare/openfeature-provider-js-v0.5.1...openfeature-provider-js-v0.6.0) (2026-01-15)
|
|
4
33
|
|
|
5
34
|
|
package/README.md
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
OpenFeature provider for the Spotify Confidence resolver (local mode, powered by WebAssembly). It periodically fetches resolver state, evaluates flags locally, and flushes evaluation logs to the Confidence backend.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
|
+
|
|
6
7
|
- Local flag evaluation via WASM (no per-eval network calls)
|
|
7
8
|
- Automatic state refresh and batched flag log flushing
|
|
8
9
|
- Pluggable `fetch` with retries, timeouts and routing
|
|
9
10
|
- Optional logging using `debug`
|
|
11
|
+
- **[React integration](./README-REACT.md)** for Next.js with Server Components
|
|
10
12
|
|
|
11
13
|
## Requirements
|
|
14
|
+
|
|
12
15
|
- Node.js 18+ (built-in `fetch`) or provide a compatible `fetch`
|
|
13
16
|
- WebAssembly support (Node 18+/modern browsers)
|
|
14
17
|
|
|
@@ -24,6 +27,7 @@ yarn add debug
|
|
|
24
27
|
```
|
|
25
28
|
|
|
26
29
|
Notes:
|
|
30
|
+
|
|
27
31
|
- `debug` is an optional peer. Install it if you want logs. Without it, logging is a no-op.
|
|
28
32
|
- Types and bundling are ESM-first; Node is supported, and a browser build is provided for modern bundlers.
|
|
29
33
|
|
|
@@ -34,6 +38,7 @@ Notes:
|
|
|
34
38
|
You'll need a **client secret** from Confidence to use this provider.
|
|
35
39
|
|
|
36
40
|
**📖 See the [Integration Guide: Getting Your Credentials](../INTEGRATION_GUIDE.md#getting-your-credentials)** for step-by-step instructions on:
|
|
41
|
+
|
|
37
42
|
- How to navigate the Confidence dashboard
|
|
38
43
|
- Creating a Backend integration
|
|
39
44
|
- Creating a test flag for verification
|
|
@@ -104,6 +109,7 @@ const context = {
|
|
|
104
109
|
The provider uses a **default value fallback** pattern - when evaluation fails, it returns your specified default value instead of throwing an error.
|
|
105
110
|
|
|
106
111
|
**📖 See the [Integration Guide: Error Handling](../INTEGRATION_GUIDE.md#error-handling)** for:
|
|
112
|
+
|
|
107
113
|
- Common failure scenarios
|
|
108
114
|
- Error codes and meanings
|
|
109
115
|
- Production best practices
|
|
@@ -119,8 +125,8 @@ const enabled = await client.getBooleanValue('my-flag.enabled', false, context);
|
|
|
119
125
|
// For detailed error information, use getBooleanDetails()
|
|
120
126
|
const details = await client.getBooleanDetails('my-flag.enabled', false, context);
|
|
121
127
|
if (details.errorCode) {
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
console.error('Flag evaluation error:', details.errorMessage);
|
|
129
|
+
console.log('Reason:', details.reason);
|
|
124
130
|
}
|
|
125
131
|
```
|
|
126
132
|
|
|
@@ -135,11 +141,85 @@ if (details.errorCode) {
|
|
|
135
141
|
- `fetch` (optional): Custom `fetch` implementation. Required for Node < 18; for Node 18+ you can omit.
|
|
136
142
|
|
|
137
143
|
The provider periodically:
|
|
144
|
+
|
|
138
145
|
- Refreshes resolver state (configurable via `stateUpdateInterval`, default every 30s)
|
|
139
146
|
- Flushes flag evaluation logs to the backend (configurable via `flushInterval`, default every 10s)
|
|
140
147
|
|
|
141
148
|
---
|
|
142
149
|
|
|
150
|
+
## Exports and WASM Loading
|
|
151
|
+
|
|
152
|
+
The package provides multiple exports for different environments:
|
|
153
|
+
|
|
154
|
+
### Default export (recommended)
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { createConfidenceServerProvider } from '@spotify-confidence/openfeature-server-provider-local';
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The WASM is **inlined as a data URL** — this is the most portable option and should work across virtually all environments. The tradeoff is a larger bundle (~700kB), but this isn't a problem for the intended server-side usage.
|
|
161
|
+
|
|
162
|
+
No configuration needed.
|
|
163
|
+
|
|
164
|
+
### `./node` — Traditional Node.js
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { createConfidenceServerProvider } from '@spotify-confidence/openfeature-server-provider-local/node';
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Uses `fs.readFile()` to load WASM from the installed package. Works well in a regular Node.js environment with node_modules.
|
|
171
|
+
|
|
172
|
+
You can customize the WASM path if needed:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const provider = createConfidenceServerProvider({
|
|
176
|
+
flagClientSecret: '...',
|
|
177
|
+
wasmPath: '/custom/path/to/confidence_resolver.wasm',
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `./fetch` — Modern and standards compliant environments
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { createConfidenceServerProvider } from '@spotify-confidence/openfeature-server-provider-local/fetch';
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Uses `fetch()` with `import.meta.url` to load WASM. Works in Deno, Bun, and browsers with bundlers that properly handle asset URLs (Vite, Rollup, etc.).
|
|
188
|
+
|
|
189
|
+
You can customize the WASM URL if needed:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const provider = createConfidenceServerProvider({
|
|
193
|
+
flagClientSecret: '...',
|
|
194
|
+
wasmUrl: '/assets/confidence_resolver.wasm',
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `./react-server` and `./react-client` — React/Next.js Integration
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// Server Component
|
|
202
|
+
import { ConfidenceProvider, getFlag, getFlagDetails } from '@spotify-confidence/openfeature-server-provider-local/react-server';
|
|
203
|
+
|
|
204
|
+
// Client Component
|
|
205
|
+
import { useFlag, useFlagDetails } from '@spotify-confidence/openfeature-server-provider-local/react-client';
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
React hooks and components for Next.js App Router with Server Components. Flags are resolved on the server and provided to client components via React Context.
|
|
209
|
+
|
|
210
|
+
**See [README-REACT.md](./README-REACT.md) for full documentation.**
|
|
211
|
+
|
|
212
|
+
### A note on browser usage
|
|
213
|
+
|
|
214
|
+
While browsers are mentioned in this doc, this package is intended for server-side use only. Two concerns for browser usage:
|
|
215
|
+
|
|
216
|
+
- Size: The WASM+JS is currently ~270kb gzipped, too large for typical client bundles.
|
|
217
|
+
- Security: In a browser, all flag rules and variants are exposed to users.
|
|
218
|
+
|
|
219
|
+
That said, the package does work in browsers, and there may be specialized use cases where these tradeoffs are acceptable.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
143
223
|
## Materialization Stores
|
|
144
224
|
|
|
145
225
|
Materialization stores provide persistent storage for sticky variant assignments and custom targeting segments. This enables two key use cases:
|
|
@@ -164,11 +244,13 @@ const provider = createConfidenceServerProvider({
|
|
|
164
244
|
```
|
|
165
245
|
|
|
166
246
|
**When to use**:
|
|
247
|
+
|
|
167
248
|
- You need sticky assignments or materialized segments but don't want to manage storage infrastructure
|
|
168
249
|
- Quick prototyping or getting started
|
|
169
250
|
- Lower-volume applications where network latency is acceptable
|
|
170
251
|
|
|
171
252
|
**Trade-offs**:
|
|
253
|
+
|
|
172
254
|
- Additional network calls during flag resolution (adds latency)
|
|
173
255
|
- Lower performance compared to local storage implementations (Redis, DynamoDB, etc.)
|
|
174
256
|
|
|
@@ -200,6 +282,7 @@ For read-only stores (e.g., pre-populated materialized segments without sticky a
|
|
|
200
282
|
### When to Use Materialization Stores
|
|
201
283
|
|
|
202
284
|
Consider implementing a materialization store if:
|
|
285
|
+
|
|
203
286
|
- You need to support sticky variant assignments for experiments
|
|
204
287
|
- You use materialized segments for custom targeting
|
|
205
288
|
- You want to minimize network latency during flag resolution
|
|
@@ -214,10 +297,12 @@ If you don't use sticky assignments or materialized segments, the default behavi
|
|
|
214
297
|
Logging uses the `debug` library if present; otherwise, all log calls are no-ops.
|
|
215
298
|
|
|
216
299
|
Namespaces:
|
|
300
|
+
|
|
217
301
|
- Core: `cnfd:*`
|
|
218
302
|
- Fetch/middleware: `cnfd:fetch:*` (e.g. retries, auth renewals, request summaries)
|
|
219
303
|
|
|
220
304
|
Log levels are hierarchical:
|
|
305
|
+
|
|
221
306
|
- `cnfd:debug` enables debug, info, warn, and error
|
|
222
307
|
- `cnfd:info` enables info, warn, and error
|
|
223
308
|
- `cnfd:warn` enables warn and error
|
|
@@ -226,6 +311,7 @@ Log levels are hierarchical:
|
|
|
226
311
|
Enable logs:
|
|
227
312
|
|
|
228
313
|
- Node:
|
|
314
|
+
|
|
229
315
|
```bash
|
|
230
316
|
DEBUG=cnfd:* node app.js
|
|
231
317
|
# or narrower
|
|
@@ -233,6 +319,7 @@ DEBUG=cnfd:info,cnfd:fetch:* node app.js
|
|
|
233
319
|
```
|
|
234
320
|
|
|
235
321
|
- Browser (in DevTools console):
|
|
322
|
+
|
|
236
323
|
```js
|
|
237
324
|
localStorage.debug = 'cnfd:*';
|
|
238
325
|
```
|
|
@@ -245,19 +332,6 @@ yarn add debug
|
|
|
245
332
|
|
|
246
333
|
---
|
|
247
334
|
|
|
248
|
-
## WebAssembly asset notes
|
|
249
|
-
|
|
250
|
-
- Node: the WASM (`confidence_resolver.wasm`) is resolved from the installed package automatically; no extra config needed.
|
|
251
|
-
- Browser: the ESM build resolves the WASM via `new URL('confidence_resolver.wasm', import.meta.url)` so modern bundlers (Vite/Rollup/Webpack 5 asset modules) will include it. If your bundler does not, configure it to treat the `.wasm` file as a static asset.
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
|
|
255
|
-
## Using in browsers
|
|
256
|
-
|
|
257
|
-
The package exports a browser ESM build that compiles the WASM via streaming and uses the global `fetch`. Integrate it with your OpenFeature SDK variant for the web similarly to Node, then register the provider before evaluation. Credentials must be available to the runtime (e.g. through your app’s configuration layer).
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
335
|
## Testing
|
|
262
336
|
|
|
263
337
|
- You can inject a custom `fetch` via the `fetch` option to stub network behavior in tests.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { EvaluationDetails, FlagValue } from "@openfeature/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
type ResolutionReason = "ERROR" | "FLAG_ARCHIVED" | "MATCH" | "NO_SEGMENT_MATCH" | "TARGETING_KEY_ERROR" | "NO_TREATMENT_MATCH" | "UNSPECIFIED";
|
|
5
|
+
declare enum ErrorCode {
|
|
6
|
+
PROVIDER_NOT_READY = "PROVIDER_NOT_READY",
|
|
7
|
+
PROVIDER_FATAL = "PROVIDER_FATAL",
|
|
8
|
+
FLAG_NOT_FOUND = "FLAG_NOT_FOUND",
|
|
9
|
+
TYPE_MISMATCH = "TYPE_MISMATCH",
|
|
10
|
+
GENERAL = "GENERAL",
|
|
11
|
+
}
|
|
12
|
+
interface ResolutionDetails<T> {
|
|
13
|
+
reason: ResolutionReason;
|
|
14
|
+
value: T;
|
|
15
|
+
variant?: string;
|
|
16
|
+
errorCode?: ErrorCode;
|
|
17
|
+
errorMessage?: string;
|
|
18
|
+
shouldApply: boolean;
|
|
19
|
+
}
|
|
20
|
+
type FlagPrimitive = null | boolean | string | number;
|
|
21
|
+
type FlagObject = {
|
|
22
|
+
[key: string]: FlagValue$1;
|
|
23
|
+
};
|
|
24
|
+
type FlagValue$1 = FlagPrimitive | FlagObject;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/flag-bundle.d.ts
|
|
27
|
+
interface FlagBundle {
|
|
28
|
+
flags: Record<string, ResolutionDetails<FlagObject | null> | undefined>;
|
|
29
|
+
resolveId: string;
|
|
30
|
+
resolveToken: string;
|
|
31
|
+
errorCode?: ErrorCode;
|
|
32
|
+
errorMessage?: string;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/react/client.d.ts
|
|
36
|
+
type FlagBundle$1 = FlagBundle;
|
|
37
|
+
type ApplyFn = (flagName: string) => Promise<void>;
|
|
38
|
+
/** @internal */
|
|
39
|
+
interface ConfidenceClientProviderProps {
|
|
40
|
+
bundle: FlagBundle$1;
|
|
41
|
+
apply: ApplyFn;
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
}
|
|
44
|
+
/** @internal */
|
|
45
|
+
declare function ConfidenceClientProvider({
|
|
46
|
+
bundle,
|
|
47
|
+
apply,
|
|
48
|
+
children
|
|
49
|
+
}: ConfidenceClientProviderProps): React.ReactElement;
|
|
50
|
+
interface UseFlagOptions {
|
|
51
|
+
/** Set to false for manual exposure control. Default is true (auto-expose on mount). */
|
|
52
|
+
expose?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Details returned by useFlagDetails hook.
|
|
56
|
+
* Always includes an expose function for manual exposure logging.
|
|
57
|
+
*/
|
|
58
|
+
interface ClientEvaluationDetails<T extends FlagValue> extends EvaluationDetails<T> {
|
|
59
|
+
/** Function to manually log exposure. No-op if already auto-exposed or if called multiple times. */
|
|
60
|
+
expose: () => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* React hook for accessing Confidence feature flag values.
|
|
64
|
+
*
|
|
65
|
+
* Automatically logs exposure when the component mounts.
|
|
66
|
+
* Supports dot notation to access nested properties within a flag value.
|
|
67
|
+
*
|
|
68
|
+
* @param flagKey - The flag key, optionally with dot notation for nested access (e.g., 'my-flag.config.enabled')
|
|
69
|
+
* @param defaultValue - Default value if flag or nested property is not found
|
|
70
|
+
* @returns The flag value (or nested property value)
|
|
71
|
+
*
|
|
72
|
+
* @example Basic usage
|
|
73
|
+
* ```tsx
|
|
74
|
+
* const enabled = useFlag('my-feature', false);
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @example Dot notation for nested properties
|
|
78
|
+
* ```tsx
|
|
79
|
+
* // Flag value: { config: { maxItems: 10, enabled: true } }
|
|
80
|
+
* const maxItems = useFlag('my-feature.config.maxItems', 5);
|
|
81
|
+
* const enabled = useFlag('my-feature.config.enabled', false);
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @see useFlagDetails for manual exposure control
|
|
85
|
+
*/
|
|
86
|
+
declare function useFlag<T extends FlagValue>(flagKey: string, defaultValue: T): T;
|
|
87
|
+
/**
|
|
88
|
+
* React hook for accessing Confidence feature flag values with full details.
|
|
89
|
+
*
|
|
90
|
+
* Returns the flag value along with variant, reason, and error information.
|
|
91
|
+
* By default, automatically logs exposure when the component mounts.
|
|
92
|
+
* Use `{ expose: false }` for manual exposure control.
|
|
93
|
+
*
|
|
94
|
+
* Supports dot notation to access nested properties within a flag value.
|
|
95
|
+
*
|
|
96
|
+
* @param flagKey - The flag key, optionally with dot notation for nested access (e.g., 'my-flag.config.enabled')
|
|
97
|
+
* @param defaultValue - Default value if flag or nested property is not found
|
|
98
|
+
* @param options - Use `{ expose: false }` for manual exposure control
|
|
99
|
+
* @returns EvaluationDetails with value, flagKey, flagMetadata, variant, reason, errorCode, errorMessage, and expose function.
|
|
100
|
+
*
|
|
101
|
+
* @example Auto exposure with full details
|
|
102
|
+
* ```tsx
|
|
103
|
+
* const { value, variant, reason } = useFlagDetails('my-feature', false);
|
|
104
|
+
* console.log(`Got ${value} from variant ${variant}, reason: ${reason}`);
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @example Manual exposure
|
|
108
|
+
* ```tsx
|
|
109
|
+
* const { value: enabled, expose } = useFlagDetails('my-feature', false, { expose: false });
|
|
110
|
+
*
|
|
111
|
+
* const handleClick = () => {
|
|
112
|
+
* if (enabled) {
|
|
113
|
+
* expose(); // Log exposure only when user interacts
|
|
114
|
+
* doSomething();
|
|
115
|
+
* }
|
|
116
|
+
* };
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @example Error handling
|
|
120
|
+
* ```tsx
|
|
121
|
+
* const { value, errorCode } = useFlagDetails('my-feature', false);
|
|
122
|
+
* if (errorCode === 'FLAG_NOT_FOUND') {
|
|
123
|
+
* console.warn('Flag not configured');
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function useFlagDetails<T extends FlagValue>(flagKey: string, defaultValue: T, options?: UseFlagOptions): ClientEvaluationDetails<T>;
|
|
128
|
+
//#endregion
|
|
129
|
+
export { ClientEvaluationDetails, ConfidenceClientProvider, ConfidenceClientProviderProps, useFlag, useFlagDetails };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useRef } from "react";
|
|
3
|
+
import "@bufbuild/protobuf/wire";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
const NOOP_LOG_FN = Object.assign(() => {}, { enabled: false });
|
|
6
|
+
const debugBackend = loadDebug();
|
|
7
|
+
const logger = new class LoggerImpl {
|
|
8
|
+
childLoggers = /* @__PURE__ */ new Map();
|
|
9
|
+
debug = NOOP_LOG_FN;
|
|
10
|
+
info = NOOP_LOG_FN;
|
|
11
|
+
warn = NOOP_LOG_FN;
|
|
12
|
+
error = NOOP_LOG_FN;
|
|
13
|
+
constructor(name) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
this.configure();
|
|
16
|
+
}
|
|
17
|
+
async configure(backend = debugBackend) {
|
|
18
|
+
const debug = await backend;
|
|
19
|
+
if (!debug) return;
|
|
20
|
+
const debugFn = this.debug = (debug(this.name + ":debug"));
|
|
21
|
+
const infoFn = this.info = (debug(this.name + ":info"));
|
|
22
|
+
const warnFn = this.warn = (debug(this.name + ":warn"));
|
|
23
|
+
const errorFn = this.error = (debug(this.name + ":error"));
|
|
24
|
+
switch (true) {
|
|
25
|
+
case debugFn.enabled: infoFn.enabled = true;
|
|
26
|
+
case infoFn.enabled: warnFn.enabled = true;
|
|
27
|
+
case warnFn.enabled: errorFn.enabled = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
getLogger(name) {
|
|
31
|
+
let child = (this.childLoggers.get(name));
|
|
32
|
+
if (!child) {
|
|
33
|
+
child = new LoggerImpl(this.name + ":" + name);
|
|
34
|
+
this.childLoggers.set(name, child);
|
|
35
|
+
}
|
|
36
|
+
return child;
|
|
37
|
+
}
|
|
38
|
+
}("cnfd");
|
|
39
|
+
logger.getLogger.bind(logger);
|
|
40
|
+
async function loadDebug() {
|
|
41
|
+
try {
|
|
42
|
+
const { default: debug } = await import("debug");
|
|
43
|
+
if (typeof debug !== "function") return null;
|
|
44
|
+
return debug;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function devWarn(message) {
|
|
50
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") console.warn(message);
|
|
51
|
+
}
|
|
52
|
+
function hasKey(obj, key) {
|
|
53
|
+
return key in obj;
|
|
54
|
+
}
|
|
55
|
+
let ErrorCode = /* @__PURE__ */ function(ErrorCode$1) {
|
|
56
|
+
ErrorCode$1["PROVIDER_NOT_READY"] = "PROVIDER_NOT_READY";
|
|
57
|
+
ErrorCode$1["PROVIDER_FATAL"] = "PROVIDER_FATAL";
|
|
58
|
+
ErrorCode$1["FLAG_NOT_FOUND"] = "FLAG_NOT_FOUND";
|
|
59
|
+
ErrorCode$1["TYPE_MISMATCH"] = "TYPE_MISMATCH";
|
|
60
|
+
ErrorCode$1["GENERAL"] = "GENERAL";
|
|
61
|
+
return ErrorCode$1;
|
|
62
|
+
}({});
|
|
63
|
+
function error(errorCode, errorMessage) {
|
|
64
|
+
return {
|
|
65
|
+
flags: {},
|
|
66
|
+
resolveId: "",
|
|
67
|
+
resolveToken: "",
|
|
68
|
+
errorCode,
|
|
69
|
+
errorMessage
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function resolve(bundle, flagKey, defaultValue, logger$1) {
|
|
73
|
+
const [flagName, ...path] = flagKey.split(".");
|
|
74
|
+
const flag = bundle?.flags[flagName];
|
|
75
|
+
if (bundle?.errorCode) {
|
|
76
|
+
logger$1?.warn(`Flag evaluation for "%s" failed. %s %s`, flagKey, bundle.errorCode, bundle?.errorMessage);
|
|
77
|
+
return {
|
|
78
|
+
reason: "ERROR",
|
|
79
|
+
errorCode: bundle.errorCode,
|
|
80
|
+
errorMessage: bundle.errorMessage,
|
|
81
|
+
value: defaultValue,
|
|
82
|
+
shouldApply: false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!flag) {
|
|
86
|
+
logger$1?.warn(`Flag evaluation for '${flagKey}' failed: flag not found`);
|
|
87
|
+
return {
|
|
88
|
+
reason: "ERROR",
|
|
89
|
+
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
|
90
|
+
value: defaultValue,
|
|
91
|
+
shouldApply: false
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
let value = flag.value;
|
|
95
|
+
for (let i = 0; i < path.length; i++) {
|
|
96
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return {
|
|
97
|
+
reason: "ERROR",
|
|
98
|
+
value: defaultValue,
|
|
99
|
+
errorCode: ErrorCode.TYPE_MISMATCH,
|
|
100
|
+
errorMessage: `resolved value is not an object at ${[flagName, ...path.slice(0, i)].join(".")}`,
|
|
101
|
+
shouldApply: false
|
|
102
|
+
};
|
|
103
|
+
value = value[path[i]];
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const validated = evaluateAssignment(value, defaultValue, [flagName, ...path]);
|
|
107
|
+
return {
|
|
108
|
+
...flag,
|
|
109
|
+
value: validated
|
|
110
|
+
};
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return {
|
|
113
|
+
reason: "ERROR",
|
|
114
|
+
value: defaultValue,
|
|
115
|
+
errorCode: ErrorCode.TYPE_MISMATCH,
|
|
116
|
+
errorMessage: String(e),
|
|
117
|
+
shouldApply: false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function evaluateAssignment(resolvedValue, defaultValue, path) {
|
|
122
|
+
const resolvedType = typeof resolvedValue;
|
|
123
|
+
const defaultType = typeof defaultValue;
|
|
124
|
+
if (Array.isArray(defaultValue)) throw `arrays are not supported as flag values at ${path.join(".")}`;
|
|
125
|
+
if (defaultValue === null) return resolvedValue;
|
|
126
|
+
if (resolvedValue === null) return defaultValue;
|
|
127
|
+
if (resolvedType !== defaultType) throw `resolved value (${resolvedType}) isn't assignable to default type (${defaultType}) at ${path.join(".")}`;
|
|
128
|
+
if (typeof resolvedValue === "object") {
|
|
129
|
+
const result = { ...resolvedValue };
|
|
130
|
+
for (const [key, value] of Object.entries(defaultValue)) {
|
|
131
|
+
if (!hasKey(resolvedValue, key)) throw `resolved value is missing field "${key}" at ${path.join(".")}`;
|
|
132
|
+
result[key] = evaluateAssignment(resolvedValue[key], value, [...path, key]);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
return resolvedValue;
|
|
137
|
+
}
|
|
138
|
+
const ConfidenceContext = createContext(null);
|
|
139
|
+
const warnedFlags = /* @__PURE__ */ new Set();
|
|
140
|
+
function ConfidenceClientProvider({ bundle, apply, children }) {
|
|
141
|
+
const appliedFlags = useRef(/* @__PURE__ */ new Set());
|
|
142
|
+
const filteredApply = useCallback((flagName) => {
|
|
143
|
+
if (appliedFlags.current.has(flagName)) return Promise.resolve();
|
|
144
|
+
appliedFlags.current.add(flagName);
|
|
145
|
+
return apply(flagName);
|
|
146
|
+
}, [apply]);
|
|
147
|
+
return /* @__PURE__ */ jsx(ConfidenceContext.Provider, {
|
|
148
|
+
value: {
|
|
149
|
+
bundle,
|
|
150
|
+
apply: filteredApply
|
|
151
|
+
},
|
|
152
|
+
children
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function useFlag(flagKey, defaultValue) {
|
|
156
|
+
return useFlagDetails(flagKey, defaultValue).value;
|
|
157
|
+
}
|
|
158
|
+
function useFlagDetails(flagKey, defaultValue, options) {
|
|
159
|
+
const ctx = useContext(ConfidenceContext);
|
|
160
|
+
if (!ctx && !warnedFlags.has(flagKey)) {
|
|
161
|
+
warnedFlags.add(flagKey);
|
|
162
|
+
devWarn(`[Confidence] useFlagDetails("${flagKey}") called without a parent ConfidenceProvider. Returning default value.`);
|
|
163
|
+
}
|
|
164
|
+
const bundle = ctx?.bundle ?? error(ErrorCode.GENERAL, "useFlagDetails called without a parent ConfidenceProvider");
|
|
165
|
+
const [baseFlagName] = flagKey.split(".", 1);
|
|
166
|
+
const resolution = resolve(bundle, flagKey, defaultValue);
|
|
167
|
+
const autoExpose = options?.expose !== false;
|
|
168
|
+
const doExpose = useCallback(() => {
|
|
169
|
+
if (resolution.shouldApply) ctx?.apply(baseFlagName);
|
|
170
|
+
}, [
|
|
171
|
+
ctx,
|
|
172
|
+
baseFlagName,
|
|
173
|
+
resolution.shouldApply
|
|
174
|
+
]);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (autoExpose) doExpose();
|
|
177
|
+
}, [autoExpose, doExpose]);
|
|
178
|
+
const expose = useCallback(() => {
|
|
179
|
+
if (autoExpose) devWarn(`[Confidence] expose() called on "${flagKey}" but auto-exposure is enabled. Call is ignored.`);
|
|
180
|
+
else doExpose();
|
|
181
|
+
}, [
|
|
182
|
+
autoExpose,
|
|
183
|
+
doExpose,
|
|
184
|
+
flagKey
|
|
185
|
+
]);
|
|
186
|
+
return {
|
|
187
|
+
flagKey,
|
|
188
|
+
flagMetadata: {},
|
|
189
|
+
...resolution,
|
|
190
|
+
expose
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export { ConfidenceClientProvider, useFlag, useFlagDetails };
|
|
Binary file
|