@getflaggy/sdk 0.1.1 → 0.2.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/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @getflaggy/sdk
2
+
3
+ JavaScript/React SDK for the [Flaggy](https://github.com/Alexis-Marcel/flaggy) feature flag server.
4
+
5
+ - Zero runtime dependencies
6
+ - Server-side evaluation with local cache
7
+ - Real-time updates via SSE
8
+ - React bindings with `useFlag` hook
9
+ - TypeScript, dual ESM/CJS
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @getflaggy/sdk
15
+ ```
16
+
17
+ ## React
18
+
19
+ ### Setup
20
+
21
+ Wrap your app with `FlaggyProvider`:
22
+
23
+ ```tsx
24
+ import { FlaggyProvider } from '@getflaggy/sdk/react';
25
+
26
+ function App() {
27
+ return (
28
+ <FlaggyProvider
29
+ serverUrl="https://flaggy.example.com"
30
+ apiKey="your-api-key"
31
+ flags={['show-banner', 'dark-mode']}
32
+ context={{ userId: '123', plan: 'pro' }}
33
+ onError={(err) => console.error(err)}
34
+ >
35
+ <MyApp />
36
+ </FlaggyProvider>
37
+ );
38
+ }
39
+ ```
40
+
41
+ ### Read a flag
42
+
43
+ ```tsx
44
+ import { useFlag } from '@getflaggy/sdk/react';
45
+
46
+ function MyComponent() {
47
+ const showBanner = useFlag('show-banner', false);
48
+
49
+ if (!showBanner) return null;
50
+ return <div>New feature!</div>;
51
+ }
52
+ ```
53
+
54
+ `useFlag(key, defaultValue)` returns the server-evaluated value, or `defaultValue` if the flag doesn't exist or the provider isn't ready yet.
55
+
56
+ ### Access client state
57
+
58
+ ```tsx
59
+ import { useFlaggy } from '@getflaggy/sdk/react';
60
+
61
+ function Status() {
62
+ const { ready, error } = useFlaggy();
63
+
64
+ if (error) return <div>Error: {error.message}</div>;
65
+ if (!ready) return <div>Loading...</div>;
66
+ return <div>Flags loaded</div>;
67
+ }
68
+ ```
69
+
70
+ ### Provider props
71
+
72
+ | Prop | Type | Default | Description |
73
+ |------|------|---------|-------------|
74
+ | `serverUrl` | `string` | — | Flaggy server URL |
75
+ | `apiKey` | `string` | — | API key |
76
+ | `flags` | `string[]` | — | List of flag keys to evaluate |
77
+ | `context` | `Record<string, unknown>` | `{}` | Evaluation context (user info, etc.) |
78
+ | `enableStreaming` | `boolean` | `true` | Enable SSE for real-time flag updates |
79
+ | `onError` | `(error: Error) => void` | — | Error callback (Sentry, Datadog, etc.) |
80
+
81
+ When `context` changes (deep comparison), flags are automatically re-evaluated.
82
+
83
+ ## Vanilla JavaScript
84
+
85
+ ```ts
86
+ import { FlaggyClient } from '@getflaggy/sdk';
87
+
88
+ const client = new FlaggyClient({
89
+ serverUrl: 'https://flaggy.example.com',
90
+ apiKey: 'your-api-key',
91
+ flags: ['show-banner', 'dark-mode'],
92
+ context: { userId: '123' },
93
+ });
94
+
95
+ client.on('ready', () => {
96
+ console.log(client.getFlag('show-banner', false));
97
+ });
98
+
99
+ client.on('change', (key, value) => {
100
+ console.log(`Flag ${key} changed to`, value);
101
+ });
102
+
103
+ client.on('error', (err) => {
104
+ console.error(err);
105
+ });
106
+
107
+ await client.initialize();
108
+
109
+ // Update context (re-evaluates all flags)
110
+ await client.setContext({ userId: '456', plan: 'enterprise' });
111
+
112
+ // Cleanup
113
+ client.destroy();
114
+ ```
115
+
116
+ ## How it works
117
+
118
+ 1. On init, the SDK calls `POST /api/v1/evaluate/batch` with the declared flag keys
119
+ 2. Results are cached locally — `getFlag()` reads from cache (synchronous)
120
+ 3. An SSE connection to `/api/v1/stream` listens for flag changes in real-time
121
+ 4. On change events, the affected flag is re-evaluated via `POST /api/v1/evaluate`
122
+ 5. SSE reconnects automatically with exponential backoff + jitter
123
+
124
+ ## License
125
+
126
+ MIT
@@ -125,6 +125,7 @@ var FlaggyClient = class {
125
125
  };
126
126
  this.serverUrl = options.serverUrl.replace(/\/$/, "");
127
127
  this.apiKey = options.apiKey;
128
+ this.flags = options.flags;
128
129
  this.context = options.context ?? {};
129
130
  this.enableStreaming = options.enableStreaming ?? true;
130
131
  this.sseRetryDelay = options.sseRetryDelay ?? 1e3;
@@ -140,7 +141,7 @@ var FlaggyClient = class {
140
141
  try {
141
142
  const response = await this.fetchApi(
142
143
  "/api/v1/evaluate/batch",
143
- { context: this.context }
144
+ { flags: this.flags, context: this.context }
144
145
  );
145
146
  this.applyBatchResult(response);
146
147
  this._ready = true;
@@ -167,7 +168,7 @@ var FlaggyClient = class {
167
168
  try {
168
169
  const response = await this.fetchApi(
169
170
  "/api/v1/evaluate/batch",
170
- { context },
171
+ { flags: this.flags, context },
171
172
  controller.signal
172
173
  );
173
174
  if (controller.signal.aborted) return;
@@ -278,4 +279,4 @@ var FlaggyClient = class {
278
279
  export {
279
280
  FlaggyClient
280
281
  };
281
- //# sourceMappingURL=chunk-HBGNXCEV.mjs.map
282
+ //# sourceMappingURL=chunk-YLOZXW2K.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sse.ts","../src/client.ts"],"sourcesContent":["import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly flags: string[];\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.flags = options.flags;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { flags: this.flags, context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { flags: this.flags, context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -151,6 +151,7 @@ var FlaggyClient = class {
151
151
  };
152
152
  this.serverUrl = options.serverUrl.replace(/\/$/, "");
153
153
  this.apiKey = options.apiKey;
154
+ this.flags = options.flags;
154
155
  this.context = options.context ?? {};
155
156
  this.enableStreaming = options.enableStreaming ?? true;
156
157
  this.sseRetryDelay = options.sseRetryDelay ?? 1e3;
@@ -166,7 +167,7 @@ var FlaggyClient = class {
166
167
  try {
167
168
  const response = await this.fetchApi(
168
169
  "/api/v1/evaluate/batch",
169
- { context: this.context }
170
+ { flags: this.flags, context: this.context }
170
171
  );
171
172
  this.applyBatchResult(response);
172
173
  this._ready = true;
@@ -193,7 +194,7 @@ var FlaggyClient = class {
193
194
  try {
194
195
  const response = await this.fetchApi(
195
196
  "/api/v1/evaluate/batch",
196
- { context },
197
+ { flags: this.flags, context },
197
198
  controller.signal
198
199
  );
199
200
  if (controller.signal.aborted) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/sse.ts","../src/client.ts"],"sourcesContent":["export { FlaggyClient } from './client';\nexport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluatedFlag,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n","import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/sse.ts","../src/client.ts"],"sourcesContent":["export { FlaggyClient } from './client';\nexport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluatedFlag,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n","import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly flags: string[];\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.flags = options.flags;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { flags: this.flags, context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { flags: this.flags, context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -29,6 +29,8 @@ interface FlagChangeEvent {
29
29
  interface FlaggyClientOptions {
30
30
  serverUrl: string;
31
31
  apiKey: string;
32
+ /** List of flag keys to evaluate */
33
+ flags: string[];
32
34
  context?: FlaggyContext;
33
35
  /** Whether to open an SSE connection for live updates. Default: true */
34
36
  enableStreaming?: boolean;
@@ -52,6 +54,7 @@ type EventMap = {
52
54
  declare class FlaggyClient {
53
55
  private readonly serverUrl;
54
56
  private readonly apiKey;
57
+ private readonly flags;
55
58
  private readonly enableStreaming;
56
59
  private readonly sseRetryDelay;
57
60
  private readonly sseMaxRetryDelay;
package/dist/index.d.ts CHANGED
@@ -29,6 +29,8 @@ interface FlagChangeEvent {
29
29
  interface FlaggyClientOptions {
30
30
  serverUrl: string;
31
31
  apiKey: string;
32
+ /** List of flag keys to evaluate */
33
+ flags: string[];
32
34
  context?: FlaggyContext;
33
35
  /** Whether to open an SSE connection for live updates. Default: true */
34
36
  enableStreaming?: boolean;
@@ -52,6 +54,7 @@ type EventMap = {
52
54
  declare class FlaggyClient {
53
55
  private readonly serverUrl;
54
56
  private readonly apiKey;
57
+ private readonly flags;
55
58
  private readonly enableStreaming;
56
59
  private readonly sseRetryDelay;
57
60
  private readonly sseMaxRetryDelay;
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FlaggyClient
3
- } from "./chunk-HBGNXCEV.mjs";
3
+ } from "./chunk-YLOZXW2K.mjs";
4
4
  export {
5
5
  FlaggyClient
6
6
  };
package/dist/react.cjs CHANGED
@@ -156,6 +156,7 @@ var FlaggyClient = class {
156
156
  };
157
157
  this.serverUrl = options.serverUrl.replace(/\/$/, "");
158
158
  this.apiKey = options.apiKey;
159
+ this.flags = options.flags;
159
160
  this.context = options.context ?? {};
160
161
  this.enableStreaming = options.enableStreaming ?? true;
161
162
  this.sseRetryDelay = options.sseRetryDelay ?? 1e3;
@@ -171,7 +172,7 @@ var FlaggyClient = class {
171
172
  try {
172
173
  const response = await this.fetchApi(
173
174
  "/api/v1/evaluate/batch",
174
- { context: this.context }
175
+ { flags: this.flags, context: this.context }
175
176
  );
176
177
  this.applyBatchResult(response);
177
178
  this._ready = true;
@@ -198,7 +199,7 @@ var FlaggyClient = class {
198
199
  try {
199
200
  const response = await this.fetchApi(
200
201
  "/api/v1/evaluate/batch",
201
- { context },
202
+ { flags: this.flags, context },
202
203
  controller.signal
203
204
  );
204
205
  if (controller.signal.aborted) return;
@@ -315,6 +316,7 @@ var import_jsx_runtime = require("react/jsx-runtime");
315
316
  function FlaggyProvider({
316
317
  serverUrl,
317
318
  apiKey,
319
+ flags,
318
320
  context,
319
321
  enableStreaming,
320
322
  onError,
@@ -328,6 +330,7 @@ function FlaggyProvider({
328
330
  const client = new FlaggyClient({
329
331
  serverUrl,
330
332
  apiKey,
333
+ flags,
331
334
  context,
332
335
  enableStreaming
333
336
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/index.ts","../src/react/FlaggyProvider.tsx","../src/sse.ts","../src/client.ts","../src/react/context.ts","../src/react/useFlag.ts","../src/react/useFlaggy.ts"],"sourcesContent":["export { FlaggyProvider } from './FlaggyProvider';\nexport type { FlaggyProviderProps } from './FlaggyProvider';\nexport { useFlag } from './useFlag';\nexport { useFlaggy } from './useFlaggy';\nexport type { FlaggyContext as FlaggyEvalContext, FlagValue } from '../types';\n","import { useEffect, useRef, useState, useMemo, type ReactNode } from 'react';\nimport { FlaggyClient } from '../client';\nimport { FlaggyReactContext } from './context';\nimport type { FlaggyContext } from '../types';\n\nexport interface FlaggyProviderProps {\n serverUrl: string;\n apiKey: string;\n context?: FlaggyContext;\n enableStreaming?: boolean;\n /** Called when an error occurs (init failure, SSE error, etc.) */\n onError?: (error: Error) => void;\n children: ReactNode;\n}\n\nexport function FlaggyProvider({\n serverUrl,\n apiKey,\n context,\n enableStreaming,\n onError,\n children,\n}: FlaggyProviderProps) {\n const clientRef = useRef<FlaggyClient | null>(null);\n const [ready, setReady] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [, setVersion] = useState(0);\n\n // Create and initialize client when serverUrl or apiKey change\n useEffect(() => {\n const client = new FlaggyClient({\n serverUrl,\n apiKey,\n context,\n enableStreaming,\n });\n clientRef.current = client;\n setReady(false);\n setError(null);\n\n const unsubReady = client.on('ready', () => setReady(true));\n const unsubError = client.on('error', (err) => {\n setError(err);\n onError?.(err);\n });\n const unsubChange = client.on('change', () => {\n setVersion((v) => v + 1);\n });\n\n client.initialize();\n\n return () => {\n unsubReady();\n unsubError();\n unsubChange();\n client.destroy();\n clientRef.current = null;\n };\n }, [serverUrl, apiKey]);\n\n // Update context when it changes (deep comparison via JSON.stringify)\n const contextKey = context ? JSON.stringify(context) : '';\n useEffect(() => {\n if (clientRef.current && context && clientRef.current.ready) {\n clientRef.current.setContext(context);\n }\n }, [contextKey]);\n\n const value = useMemo(\n () =>\n clientRef.current\n ? { client: clientRef.current, ready, error }\n : null,\n [ready, error],\n );\n\n if (!value) return null;\n\n return (\n <FlaggyReactContext.Provider value={value}>\n {children}\n </FlaggyReactContext.Provider>\n );\n}\n","import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n","import { createContext } from 'react';\nimport type { FlaggyClient } from '../client';\n\nexport interface FlaggyContextValue {\n client: FlaggyClient;\n ready: boolean;\n error: Error | null;\n}\n\nexport const FlaggyReactContext = createContext<FlaggyContextValue | null>(null);\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\nimport type { FlagValue } from '../types';\n\nexport function useFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n return defaultValue;\n }\n\n return ctx.client.getFlag(key, defaultValue);\n}\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\n\nexport function useFlaggy() {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n throw new Error('[flaggy] useFlaggy() must be used within a <FlaggyProvider>.');\n }\n\n return {\n client: ctx.client,\n ready: ctx.ready,\n error: ctx.error,\n };\n}\n"],"mappings":"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","names":["import_react","import_react","import_react"]}
1
+ {"version":3,"sources":["../src/react/index.ts","../src/react/FlaggyProvider.tsx","../src/sse.ts","../src/client.ts","../src/react/context.ts","../src/react/useFlag.ts","../src/react/useFlaggy.ts"],"sourcesContent":["export { FlaggyProvider } from './FlaggyProvider';\nexport type { FlaggyProviderProps } from './FlaggyProvider';\nexport { useFlag } from './useFlag';\nexport { useFlaggy } from './useFlaggy';\nexport type { FlaggyContext as FlaggyEvalContext, FlagValue } from '../types';\n","import { useEffect, useRef, useState, useMemo, type ReactNode } from 'react';\nimport { FlaggyClient } from '../client';\nimport { FlaggyReactContext } from './context';\nimport type { FlaggyContext } from '../types';\n\nexport interface FlaggyProviderProps {\n serverUrl: string;\n apiKey: string;\n flags: string[];\n context?: FlaggyContext;\n enableStreaming?: boolean;\n /** Called when an error occurs (init failure, SSE error, etc.) */\n onError?: (error: Error) => void;\n children: ReactNode;\n}\n\nexport function FlaggyProvider({\n serverUrl,\n apiKey,\n flags,\n context,\n enableStreaming,\n onError,\n children,\n}: FlaggyProviderProps) {\n const clientRef = useRef<FlaggyClient | null>(null);\n const [ready, setReady] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [, setVersion] = useState(0);\n\n // Create and initialize client when serverUrl or apiKey change\n useEffect(() => {\n const client = new FlaggyClient({\n serverUrl,\n apiKey,\n flags,\n context,\n enableStreaming,\n });\n clientRef.current = client;\n setReady(false);\n setError(null);\n\n const unsubReady = client.on('ready', () => setReady(true));\n const unsubError = client.on('error', (err) => {\n setError(err);\n onError?.(err);\n });\n const unsubChange = client.on('change', () => {\n setVersion((v) => v + 1);\n });\n\n client.initialize();\n\n return () => {\n unsubReady();\n unsubError();\n unsubChange();\n client.destroy();\n clientRef.current = null;\n };\n }, [serverUrl, apiKey]);\n\n // Update context when it changes (deep comparison via JSON.stringify)\n const contextKey = context ? JSON.stringify(context) : '';\n useEffect(() => {\n if (clientRef.current && context && clientRef.current.ready) {\n clientRef.current.setContext(context);\n }\n }, [contextKey]);\n\n const value = useMemo(\n () =>\n clientRef.current\n ? { client: clientRef.current, ready, error }\n : null,\n [ready, error],\n );\n\n if (!value) return null;\n\n return (\n <FlaggyReactContext.Provider value={value}>\n {children}\n </FlaggyReactContext.Provider>\n );\n}\n","import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly flags: string[];\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.flags = options.flags;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { flags: this.flags, context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { flags: this.flags, context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n","import { createContext } from 'react';\nimport type { FlaggyClient } from '../client';\n\nexport interface FlaggyContextValue {\n client: FlaggyClient;\n ready: boolean;\n error: Error | null;\n}\n\nexport const FlaggyReactContext = createContext<FlaggyContextValue | null>(null);\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\nimport type { FlagValue } from '../types';\n\nexport function useFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n return defaultValue;\n }\n\n return ctx.client.getFlag(key, defaultValue);\n}\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\n\nexport function useFlaggy() {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n throw new Error('[flaggy] useFlaggy() must be used within a <FlaggyProvider>.');\n }\n\n return {\n client: ctx.client,\n ready: ctx.ready,\n error: ctx.error,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,gBAAqE;;;ACW9D,IAAM,aAAN,MAAiB;AAAA,EAatB,YAAY,SAA4B;AAZxC,SAAQ,kBAA0C;AAClD,SAAQ,aAAa;AACrB,SAAQ,eAAqD;AAC7D,SAAQ,YAAY;AAUlB,SAAK,MAAM,QAAQ;AACnB,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AACvB,SAAK,UAAU,QAAQ;AACvB,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,gBAAgB,QAAQ,iBAAiB;AAAA,EAChD;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,UAAW;AAEpB,SAAK,kBAAkB,IAAI,gBAAgB;AAE3C,UAAM,KAAK,KAAK;AAAA,MACd,SAAS;AAAA,QACP,eAAe,UAAU,KAAK,MAAM;AAAA,QACpC,QAAQ;AAAA,MACV;AAAA,MACA,QAAQ,KAAK,gBAAgB;AAAA,IAC/B,CAAC,EACE,KAAK,CAAC,aAAa;AAClB,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,EAAE;AAAA,MAC7D;AACA,UAAI,CAAC,SAAS,MAAM;AAClB,cAAM,IAAI,MAAM,0BAA0B;AAAA,MAC5C;AAEA,WAAK,aAAa;AAClB,WAAK,WAAW,SAAS,IAAI;AAAA,IAC/B,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,UAAI,KAAK,UAAW;AACpB,UAAI,eAAe,SAAS,IAAI,SAAS,aAAc;AAEvD,WAAK,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAChE,WAAK,UAAU;AAAA,IACjB,CAAC;AAAA,EACL;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY;AACjB,SAAK,iBAAiB,MAAM;AAC5B,SAAK,kBAAkB;AACvB,QAAI,KAAK,cAAc;AACrB,mBAAa,KAAK,YAAY;AAC9B,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAc,WAAW,MAAiD;AACxE,UAAM,SAAS,KAAK,UAAU;AAC9B,UAAM,UAAU,IAAI,YAAY;AAChC,QAAI,SAAS;AACb,QAAI,eAAe;AACnB,QAAI,cAAc;AAElB,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,YAAI,KAAM;AAEV,kBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAEhD,cAAM,QAAQ,OAAO,MAAM,IAAI;AAE/B,iBAAS,MAAM,IAAI,KAAK;AAExB,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,WAAW,QAAQ,GAAG;AAC7B,2BAAe,KAAK,MAAM,CAAC,EAAE,KAAK;AAAA,UACpC,WAAW,KAAK,WAAW,OAAO,GAAG;AACnC,0BAAc,KAAK,MAAM,CAAC,EAAE,KAAK;AAAA,UACnC,WAAW,SAAS,IAAI;AAEtB,gBAAI,aAAa;AACf,mBAAK,YAAY,cAAc,WAAW;AAAA,YAC5C;AACA,2BAAe;AACf,0BAAc;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,KAAc;AACrB,UAAI,KAAK,UAAW;AACpB,UAAI,eAAe,SAAS,IAAI,SAAS,aAAc;AAEvD,WAAK,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAClE,UAAE;AACA,aAAO,YAAY;AAAA,IACrB;AAGA,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAmB,MAAoB;AACzD,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,IAAI;AAE9B,UAAI,aAAa,CAAC,OAAO,MAAM;AAC7B,eAAO,OAAO;AAAA,MAChB;AACA,WAAK,QAAQ,MAAM;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,YAAkB;AACxB,QAAI,KAAK,UAAW;AAEpB,UAAM,QAAQ,KAAK,gBAAgB;AACnC,SAAK;AACL,SAAK,eAAe,WAAW,MAAM;AACnC,WAAK,eAAe;AACpB,WAAK,QAAQ;AAAA,IACf,GAAG,KAAK;AAAA,EACV;AAAA,EAEQ,kBAA0B;AAChC,UAAM,QAAQ,KAAK,aAAa,KAAK,IAAI,GAAG,KAAK,UAAU;AAC3D,UAAM,SAAS,QAAQ,QAAQ,KAAK,OAAO,IAAI,IAAI;AACnD,WAAO,KAAK,IAAI,QAAQ,QAAQ,KAAK,aAAa;AAAA,EACpD;AACF;;;ACrIO,IAAM,eAAN,MAAmB;AAAA,EAyBxB,YAAY,SAA8B;AAhB1C,SAAQ,QAAQ,oBAAI,IAAuB;AAC3C,SAAQ,SAAS;AACjB,SAAQ,SAAuB;AAC/B,SAAQ,aAAgC;AACxC,SAAQ,yBAAiD;AAEzD,SAAQ,YAIJ;AAAA,MACF,QAAQ,oBAAI,IAAI;AAAA,MAChB,OAAO,oBAAI,IAAI;AAAA,MACf,OAAO,oBAAI,IAAI;AAAA,IACjB;AAGE,SAAK,YAAY,QAAQ,UAAU,QAAQ,OAAO,EAAE;AACpD,SAAK,SAAS,QAAQ;AACtB,SAAK,QAAQ,QAAQ;AACrB,SAAK,UAAU,QAAQ,WAAW,CAAC;AACnC,SAAK,kBAAkB,QAAQ,mBAAmB;AAClD,SAAK,gBAAgB,QAAQ,iBAAiB;AAC9C,SAAK,mBAAmB,QAAQ,oBAAoB;AAAA,EACtD;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,aAA4B;AAChC,QAAI;AACF,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,QACA,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,QAAQ;AAAA,MAC7C;AACA,WAAK,iBAAiB,QAAQ;AAC9B,WAAK,SAAS;AACd,WAAK,KAAK,OAAO;AAAA,IACnB,SAAS,KAAc;AACrB,WAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,WAAK,KAAK,SAAS,KAAK,MAAM;AAAA,IAChC;AAEA,QAAI,KAAK,iBAAiB;AACxB,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,QAA6B,KAAa,cAAoB;AAC5D,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,MAAM,IAAI,GAAG,GAAG;AACxC,aAAO;AAAA,IACT;AACA,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EAEA,MAAM,WAAW,SAAuC;AACtD,SAAK,UAAU;AACf,SAAK,wBAAwB,MAAM;AACnC,UAAM,aAAa,IAAI,gBAAgB;AACvC,SAAK,yBAAyB;AAE9B,QAAI;AACF,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,QACA,EAAE,OAAO,KAAK,OAAO,QAAQ;AAAA,QAC7B,WAAW;AAAA,MACb;AACA,UAAI,WAAW,OAAO,QAAS;AAC/B,WAAK,iBAAiB,QAAQ;AAAA,IAChC,SAAS,KAAc;AACrB,UAAI,eAAe,SAAS,IAAI,SAAS,aAAc;AACvD,WAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,WAAK,KAAK,SAAS,KAAK,MAAM;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,GAA6B,OAAU,UAAmC;AACxE,IAAC,KAAK,UAAU,KAAK,EAAuB,IAAI,QAAQ;AACxD,WAAO,MAAM;AACX,MAAC,KAAK,UAAU,KAAK,EAAuB,OAAO,QAAQ;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY,QAAQ;AACzB,SAAK,aAAa;AAClB,SAAK,wBAAwB,MAAM;AACnC,SAAK,yBAAyB;AAC9B,SAAK,UAAU,OAAO,MAAM;AAC5B,SAAK,UAAU,MAAM,MAAM;AAC3B,SAAK,UAAU,MAAM,MAAM;AAAA,EAC7B;AAAA,EAEQ,WAAiB;AACvB,SAAK,aAAa,IAAI,WAAW;AAAA,MAC/B,KAAK,GAAG,KAAK,SAAS;AAAA,MACtB,QAAQ,KAAK;AAAA,MACb,SAAS,CAAC,UAAU,KAAK,eAAe,KAAK;AAAA,MAC7C,SAAS,CAAC,QAAQ,KAAK,KAAK,SAAS,GAAG;AAAA,MACxC,YAAY,KAAK;AAAA,MACjB,eAAe,KAAK;AAAA,IACtB,CAAC;AACD,SAAK,WAAW,QAAQ;AAAA,EAC1B;AAAA,EAEA,MAAc,eAAe,OAAuC;AAClE,QAAI,MAAM,SAAS,gBAAgB;AACjC,UAAI,KAAK,MAAM,IAAI,MAAM,GAAG,GAAG;AAC7B,aAAK,MAAM,OAAO,MAAM,GAAG;AAC3B,aAAK,KAAK,UAAU,MAAM,KAAK,MAAiC;AAAA,MAClE;AACA;AAAA,IACF;AAGA,QAAI;AACF,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,QACA,EAAE,UAAU,MAAM,KAAK,SAAS,KAAK,QAAQ;AAAA,MAC/C;AACA,YAAM,WAAW,KAAK,MAAM,IAAI,MAAM,GAAG;AACzC,WAAK,MAAM,IAAI,MAAM,KAAK,SAAS,KAAK;AACxC,UAAI,aAAa,SAAS,OAAO;AAC/B,aAAK,KAAK,UAAU,MAAM,KAAK,SAAS,KAAK;AAAA,MAC/C;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,iBAAiB,UAAuC;AAC9D,UAAM,WAAW,oBAAI,IAAuB;AAC5C,eAAW,QAAQ,SAAS,SAAS;AACnC,eAAS,IAAI,KAAK,UAAU,KAAK,KAAK;AAAA,IACxC;AAGA,eAAW,CAAC,KAAK,QAAQ,KAAK,UAAU;AACtC,YAAM,WAAW,KAAK,MAAM,IAAI,GAAG;AACnC,UAAI,aAAa,UAAU;AACzB,aAAK,KAAK,UAAU,KAAK,QAAQ;AAAA,MACnC;AAAA,IACF;AAGA,eAAW,OAAO,KAAK,MAAM,KAAK,GAAG;AACnC,UAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AACtB,aAAK,KAAK,UAAU,KAAK,MAAiC;AAAA,MAC5D;AAAA,IACF;AAEA,SAAK,QAAQ;AAAA,EACf;AAAA,EAKQ,KAAK,UAA0B,MAAuB;AAC5D,QAAI,UAAU,UAAU;AACtB,iBAAW,YAAY,KAAK,UAAU,QAAQ;AAC5C,iBAAS,KAAK,CAAC,GAAa,KAAK,CAAC,CAAc;AAAA,MAClD;AAAA,IACF,WAAW,UAAU,SAAS;AAC5B,iBAAW,YAAY,KAAK,UAAU,OAAO;AAC3C,iBAAS;AAAA,MACX;AAAA,IACF,WAAW,UAAU,SAAS;AAC5B,iBAAW,YAAY,KAAK,UAAU,OAAO;AAC3C,iBAAS,KAAK,CAAC,CAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,SACZ,MACA,MACA,QACY;AACZ,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,SAAS,GAAG,IAAI,IAAI;AAAA,MACvD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,MAAM;AAAA,MACtC;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB;AAAA,IACF,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,qBAAqB,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,IAC/E;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;;;ACjOA,mBAA8B;AASvB,IAAM,yBAAqB,4BAAyC,IAAI;;;AHyE3E;AAlEG,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,gBAAY,sBAA4B,IAAI;AAClD,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAS,KAAK;AACxC,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAuB,IAAI;AACrD,QAAM,CAAC,EAAE,UAAU,QAAI,wBAAS,CAAC;AAGjC,+BAAU,MAAM;AACd,UAAM,SAAS,IAAI,aAAa;AAAA,MAC9B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,cAAU,UAAU;AACpB,aAAS,KAAK;AACd,aAAS,IAAI;AAEb,UAAM,aAAa,OAAO,GAAG,SAAS,MAAM,SAAS,IAAI,CAAC;AAC1D,UAAM,aAAa,OAAO,GAAG,SAAS,CAAC,QAAQ;AAC7C,eAAS,GAAG;AACZ,gBAAU,GAAG;AAAA,IACf,CAAC;AACD,UAAM,cAAc,OAAO,GAAG,UAAU,MAAM;AAC5C,iBAAW,CAAC,MAAM,IAAI,CAAC;AAAA,IACzB,CAAC;AAED,WAAO,WAAW;AAElB,WAAO,MAAM;AACX,iBAAW;AACX,iBAAW;AACX,kBAAY;AACZ,aAAO,QAAQ;AACf,gBAAU,UAAU;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,MAAM,CAAC;AAGtB,QAAM,aAAa,UAAU,KAAK,UAAU,OAAO,IAAI;AACvD,+BAAU,MAAM;AACd,QAAI,UAAU,WAAW,WAAW,UAAU,QAAQ,OAAO;AAC3D,gBAAU,QAAQ,WAAW,OAAO;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,YAAQ;AAAA,IACZ,MACE,UAAU,UACN,EAAE,QAAQ,UAAU,SAAS,OAAO,MAAM,IAC1C;AAAA,IACN,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,4CAAC,mBAAmB,UAAnB,EAA4B,OAC1B,UACH;AAEJ;;;AItFA,IAAAC,gBAA2B;AAIpB,SAAS,QAA6B,KAAa,cAAoB;AAC5E,QAAM,UAAM,0BAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,SAAO,IAAI,OAAO,QAAQ,KAAK,YAAY;AAC7C;;;ACZA,IAAAC,gBAA2B;AAGpB,SAAS,YAAY;AAC1B,QAAM,UAAM,0BAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF;AAEA,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,OAAO,IAAI;AAAA,EACb;AACF;","names":["import_react","import_react","import_react"]}
package/dist/react.d.cts CHANGED
@@ -5,13 +5,14 @@ import { FlaggyContext, FlagValue, FlaggyClient } from './index.cjs';
5
5
  interface FlaggyProviderProps {
6
6
  serverUrl: string;
7
7
  apiKey: string;
8
+ flags: string[];
8
9
  context?: FlaggyContext;
9
10
  enableStreaming?: boolean;
10
11
  /** Called when an error occurs (init failure, SSE error, etc.) */
11
12
  onError?: (error: Error) => void;
12
13
  children: ReactNode;
13
14
  }
14
- declare function FlaggyProvider({ serverUrl, apiKey, context, enableStreaming, onError, children, }: FlaggyProviderProps): react_jsx_runtime.JSX.Element | null;
15
+ declare function FlaggyProvider({ serverUrl, apiKey, flags, context, enableStreaming, onError, children, }: FlaggyProviderProps): react_jsx_runtime.JSX.Element | null;
15
16
 
16
17
  declare function useFlag<T extends FlagValue>(key: string, defaultValue: T): T;
17
18
 
package/dist/react.d.ts CHANGED
@@ -5,13 +5,14 @@ import { FlaggyContext, FlagValue, FlaggyClient } from './index.js';
5
5
  interface FlaggyProviderProps {
6
6
  serverUrl: string;
7
7
  apiKey: string;
8
+ flags: string[];
8
9
  context?: FlaggyContext;
9
10
  enableStreaming?: boolean;
10
11
  /** Called when an error occurs (init failure, SSE error, etc.) */
11
12
  onError?: (error: Error) => void;
12
13
  children: ReactNode;
13
14
  }
14
- declare function FlaggyProvider({ serverUrl, apiKey, context, enableStreaming, onError, children, }: FlaggyProviderProps): react_jsx_runtime.JSX.Element | null;
15
+ declare function FlaggyProvider({ serverUrl, apiKey, flags, context, enableStreaming, onError, children, }: FlaggyProviderProps): react_jsx_runtime.JSX.Element | null;
15
16
 
16
17
  declare function useFlag<T extends FlagValue>(key: string, defaultValue: T): T;
17
18
 
package/dist/react.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FlaggyClient
3
- } from "./chunk-HBGNXCEV.mjs";
3
+ } from "./chunk-YLOZXW2K.mjs";
4
4
 
5
5
  // src/react/FlaggyProvider.tsx
6
6
  import { useEffect, useRef, useState, useMemo } from "react";
@@ -14,6 +14,7 @@ import { jsx } from "react/jsx-runtime";
14
14
  function FlaggyProvider({
15
15
  serverUrl,
16
16
  apiKey,
17
+ flags,
17
18
  context,
18
19
  enableStreaming,
19
20
  onError,
@@ -27,6 +28,7 @@ function FlaggyProvider({
27
28
  const client = new FlaggyClient({
28
29
  serverUrl,
29
30
  apiKey,
31
+ flags,
30
32
  context,
31
33
  enableStreaming
32
34
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/FlaggyProvider.tsx","../src/react/context.ts","../src/react/useFlag.ts","../src/react/useFlaggy.ts"],"sourcesContent":["import { useEffect, useRef, useState, useMemo, type ReactNode } from 'react';\nimport { FlaggyClient } from '../client';\nimport { FlaggyReactContext } from './context';\nimport type { FlaggyContext } from '../types';\n\nexport interface FlaggyProviderProps {\n serverUrl: string;\n apiKey: string;\n context?: FlaggyContext;\n enableStreaming?: boolean;\n /** Called when an error occurs (init failure, SSE error, etc.) */\n onError?: (error: Error) => void;\n children: ReactNode;\n}\n\nexport function FlaggyProvider({\n serverUrl,\n apiKey,\n context,\n enableStreaming,\n onError,\n children,\n}: FlaggyProviderProps) {\n const clientRef = useRef<FlaggyClient | null>(null);\n const [ready, setReady] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [, setVersion] = useState(0);\n\n // Create and initialize client when serverUrl or apiKey change\n useEffect(() => {\n const client = new FlaggyClient({\n serverUrl,\n apiKey,\n context,\n enableStreaming,\n });\n clientRef.current = client;\n setReady(false);\n setError(null);\n\n const unsubReady = client.on('ready', () => setReady(true));\n const unsubError = client.on('error', (err) => {\n setError(err);\n onError?.(err);\n });\n const unsubChange = client.on('change', () => {\n setVersion((v) => v + 1);\n });\n\n client.initialize();\n\n return () => {\n unsubReady();\n unsubError();\n unsubChange();\n client.destroy();\n clientRef.current = null;\n };\n }, [serverUrl, apiKey]);\n\n // Update context when it changes (deep comparison via JSON.stringify)\n const contextKey = context ? JSON.stringify(context) : '';\n useEffect(() => {\n if (clientRef.current && context && clientRef.current.ready) {\n clientRef.current.setContext(context);\n }\n }, [contextKey]);\n\n const value = useMemo(\n () =>\n clientRef.current\n ? { client: clientRef.current, ready, error }\n : null,\n [ready, error],\n );\n\n if (!value) return null;\n\n return (\n <FlaggyReactContext.Provider value={value}>\n {children}\n </FlaggyReactContext.Provider>\n );\n}\n","import { createContext } from 'react';\nimport type { FlaggyClient } from '../client';\n\nexport interface FlaggyContextValue {\n client: FlaggyClient;\n ready: boolean;\n error: Error | null;\n}\n\nexport const FlaggyReactContext = createContext<FlaggyContextValue | null>(null);\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\nimport type { FlagValue } from '../types';\n\nexport function useFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n return defaultValue;\n }\n\n return ctx.client.getFlag(key, defaultValue);\n}\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\n\nexport function useFlaggy() {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n throw new Error('[flaggy] useFlaggy() must be used within a <FlaggyProvider>.');\n }\n\n return {\n client: ctx.client,\n ready: ctx.ready,\n error: ctx.error,\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,WAAW,QAAQ,UAAU,eAA+B;;;ACArE,SAAS,qBAAqB;AASvB,IAAM,qBAAqB,cAAyC,IAAI;;;ADsE3E;AAhEG,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,YAAY,OAA4B,IAAI;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,EAAE,UAAU,IAAI,SAAS,CAAC;AAGjC,YAAU,MAAM;AACd,UAAM,SAAS,IAAI,aAAa;AAAA,MAC9B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,cAAU,UAAU;AACpB,aAAS,KAAK;AACd,aAAS,IAAI;AAEb,UAAM,aAAa,OAAO,GAAG,SAAS,MAAM,SAAS,IAAI,CAAC;AAC1D,UAAM,aAAa,OAAO,GAAG,SAAS,CAAC,QAAQ;AAC7C,eAAS,GAAG;AACZ,gBAAU,GAAG;AAAA,IACf,CAAC;AACD,UAAM,cAAc,OAAO,GAAG,UAAU,MAAM;AAC5C,iBAAW,CAAC,MAAM,IAAI,CAAC;AAAA,IACzB,CAAC;AAED,WAAO,WAAW;AAElB,WAAO,MAAM;AACX,iBAAW;AACX,iBAAW;AACX,kBAAY;AACZ,aAAO,QAAQ;AACf,gBAAU,UAAU;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,MAAM,CAAC;AAGtB,QAAM,aAAa,UAAU,KAAK,UAAU,OAAO,IAAI;AACvD,YAAU,MAAM;AACd,QAAI,UAAU,WAAW,WAAW,UAAU,QAAQ,OAAO;AAC3D,gBAAU,QAAQ,WAAW,OAAO;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,QAAQ;AAAA,IACZ,MACE,UAAU,UACN,EAAE,QAAQ,UAAU,SAAS,OAAO,MAAM,IAC1C;AAAA,IACN,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,oBAAC,mBAAmB,UAAnB,EAA4B,OAC1B,UACH;AAEJ;;;AEnFA,SAAS,kBAAkB;AAIpB,SAAS,QAA6B,KAAa,cAAoB;AAC5E,QAAM,MAAM,WAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,SAAO,IAAI,OAAO,QAAQ,KAAK,YAAY;AAC7C;;;ACZA,SAAS,cAAAA,mBAAkB;AAGpB,SAAS,YAAY;AAC1B,QAAM,MAAMC,YAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF;AAEA,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,OAAO,IAAI;AAAA,EACb;AACF;","names":["useContext","useContext"]}
1
+ {"version":3,"sources":["../src/react/FlaggyProvider.tsx","../src/react/context.ts","../src/react/useFlag.ts","../src/react/useFlaggy.ts"],"sourcesContent":["import { useEffect, useRef, useState, useMemo, type ReactNode } from 'react';\nimport { FlaggyClient } from '../client';\nimport { FlaggyReactContext } from './context';\nimport type { FlaggyContext } from '../types';\n\nexport interface FlaggyProviderProps {\n serverUrl: string;\n apiKey: string;\n flags: string[];\n context?: FlaggyContext;\n enableStreaming?: boolean;\n /** Called when an error occurs (init failure, SSE error, etc.) */\n onError?: (error: Error) => void;\n children: ReactNode;\n}\n\nexport function FlaggyProvider({\n serverUrl,\n apiKey,\n flags,\n context,\n enableStreaming,\n onError,\n children,\n}: FlaggyProviderProps) {\n const clientRef = useRef<FlaggyClient | null>(null);\n const [ready, setReady] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [, setVersion] = useState(0);\n\n // Create and initialize client when serverUrl or apiKey change\n useEffect(() => {\n const client = new FlaggyClient({\n serverUrl,\n apiKey,\n flags,\n context,\n enableStreaming,\n });\n clientRef.current = client;\n setReady(false);\n setError(null);\n\n const unsubReady = client.on('ready', () => setReady(true));\n const unsubError = client.on('error', (err) => {\n setError(err);\n onError?.(err);\n });\n const unsubChange = client.on('change', () => {\n setVersion((v) => v + 1);\n });\n\n client.initialize();\n\n return () => {\n unsubReady();\n unsubError();\n unsubChange();\n client.destroy();\n clientRef.current = null;\n };\n }, [serverUrl, apiKey]);\n\n // Update context when it changes (deep comparison via JSON.stringify)\n const contextKey = context ? JSON.stringify(context) : '';\n useEffect(() => {\n if (clientRef.current && context && clientRef.current.ready) {\n clientRef.current.setContext(context);\n }\n }, [contextKey]);\n\n const value = useMemo(\n () =>\n clientRef.current\n ? { client: clientRef.current, ready, error }\n : null,\n [ready, error],\n );\n\n if (!value) return null;\n\n return (\n <FlaggyReactContext.Provider value={value}>\n {children}\n </FlaggyReactContext.Provider>\n );\n}\n","import { createContext } from 'react';\nimport type { FlaggyClient } from '../client';\n\nexport interface FlaggyContextValue {\n client: FlaggyClient;\n ready: boolean;\n error: Error | null;\n}\n\nexport const FlaggyReactContext = createContext<FlaggyContextValue | null>(null);\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\nimport type { FlagValue } from '../types';\n\nexport function useFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n return defaultValue;\n }\n\n return ctx.client.getFlag(key, defaultValue);\n}\n","import { useContext } from 'react';\nimport { FlaggyReactContext } from './context';\n\nexport function useFlaggy() {\n const ctx = useContext(FlaggyReactContext);\n\n if (!ctx) {\n throw new Error('[flaggy] useFlaggy() must be used within a <FlaggyProvider>.');\n }\n\n return {\n client: ctx.client,\n ready: ctx.ready,\n error: ctx.error,\n };\n}\n"],"mappings":";;;;;AAAA,SAAS,WAAW,QAAQ,UAAU,eAA+B;;;ACArE,SAAS,qBAAqB;AASvB,IAAM,qBAAqB,cAAyC,IAAI;;;ADyE3E;AAlEG,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,YAAY,OAA4B,IAAI;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,EAAE,UAAU,IAAI,SAAS,CAAC;AAGjC,YAAU,MAAM;AACd,UAAM,SAAS,IAAI,aAAa;AAAA,MAC9B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,cAAU,UAAU;AACpB,aAAS,KAAK;AACd,aAAS,IAAI;AAEb,UAAM,aAAa,OAAO,GAAG,SAAS,MAAM,SAAS,IAAI,CAAC;AAC1D,UAAM,aAAa,OAAO,GAAG,SAAS,CAAC,QAAQ;AAC7C,eAAS,GAAG;AACZ,gBAAU,GAAG;AAAA,IACf,CAAC;AACD,UAAM,cAAc,OAAO,GAAG,UAAU,MAAM;AAC5C,iBAAW,CAAC,MAAM,IAAI,CAAC;AAAA,IACzB,CAAC;AAED,WAAO,WAAW;AAElB,WAAO,MAAM;AACX,iBAAW;AACX,iBAAW;AACX,kBAAY;AACZ,aAAO,QAAQ;AACf,gBAAU,UAAU;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,MAAM,CAAC;AAGtB,QAAM,aAAa,UAAU,KAAK,UAAU,OAAO,IAAI;AACvD,YAAU,MAAM;AACd,QAAI,UAAU,WAAW,WAAW,UAAU,QAAQ,OAAO;AAC3D,gBAAU,QAAQ,WAAW,OAAO;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,QAAQ;AAAA,IACZ,MACE,UAAU,UACN,EAAE,QAAQ,UAAU,SAAS,OAAO,MAAM,IAC1C;AAAA,IACN,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,oBAAC,mBAAmB,UAAnB,EAA4B,OAC1B,UACH;AAEJ;;;AEtFA,SAAS,kBAAkB;AAIpB,SAAS,QAA6B,KAAa,cAAoB;AAC5E,QAAM,MAAM,WAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,SAAO,IAAI,OAAO,QAAQ,KAAK,YAAY;AAC7C;;;ACZA,SAAS,cAAAA,mBAAkB;AAGpB,SAAS,YAAY;AAC1B,QAAM,MAAMC,YAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF;AAEA,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,OAAO,IAAI;AAAA,EACb;AACF;","names":["useContext","useContext"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getflaggy/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "JavaScript/React SDK for the Flaggy feature flag server",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/sse.ts","../src/client.ts"],"sourcesContent":["import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}