@getflaggy/sdk 0.2.1 → 0.2.3
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/dist/{chunk-YLOZXW2K.mjs → chunk-K665S5QT.mjs} +15 -18
- package/dist/chunk-K665S5QT.mjs.map +1 -0
- package/dist/index.cjs +14 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.mjs +1 -1
- package/dist/react.cjs +17 -20
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +4 -4
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-YLOZXW2K.mjs.map +0 -1
|
@@ -32,7 +32,7 @@ var SSEManager = class {
|
|
|
32
32
|
this.readStream(response.body);
|
|
33
33
|
}).catch((err) => {
|
|
34
34
|
if (this.destroyed) return;
|
|
35
|
-
if (err
|
|
35
|
+
if (this.isAbortError(err)) return;
|
|
36
36
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
37
37
|
this.reconnect();
|
|
38
38
|
});
|
|
@@ -75,7 +75,7 @@ var SSEManager = class {
|
|
|
75
75
|
}
|
|
76
76
|
} catch (err) {
|
|
77
77
|
if (this.destroyed) return;
|
|
78
|
-
if (err
|
|
78
|
+
if (this.isAbortError(err)) return;
|
|
79
79
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
80
80
|
} finally {
|
|
81
81
|
reader.releaseLock();
|
|
@@ -87,7 +87,7 @@ var SSEManager = class {
|
|
|
87
87
|
handleEvent(eventType, data) {
|
|
88
88
|
try {
|
|
89
89
|
const parsed = JSON.parse(data);
|
|
90
|
-
if (eventType
|
|
90
|
+
if (eventType) {
|
|
91
91
|
parsed.type = eventType;
|
|
92
92
|
}
|
|
93
93
|
this.onEvent(parsed);
|
|
@@ -103,6 +103,13 @@ var SSEManager = class {
|
|
|
103
103
|
this.connect();
|
|
104
104
|
}, delay);
|
|
105
105
|
}
|
|
106
|
+
/** Safari/WebKit throws TypeError instead of AbortError when a fetch is aborted */
|
|
107
|
+
isAbortError(err) {
|
|
108
|
+
if (!(err instanceof Error)) return false;
|
|
109
|
+
if (err.name === "AbortError") return true;
|
|
110
|
+
if (err.name === "TypeError" && /load failed|cancelled/i.test(err.message)) return true;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
106
113
|
getBackoffDelay() {
|
|
107
114
|
const delay = this.retryDelay * Math.pow(2, this.retryCount);
|
|
108
115
|
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
@@ -206,23 +213,13 @@ var FlaggyClient = class {
|
|
|
206
213
|
this.sseManager.connect();
|
|
207
214
|
}
|
|
208
215
|
async handleSSEEvent(event) {
|
|
209
|
-
if (event.type === "
|
|
210
|
-
if (this.cache.has(event.key)) {
|
|
211
|
-
this.cache.delete(event.key);
|
|
212
|
-
this.emit("change", event.key, void 0);
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
+
if (event.type === "connected") return;
|
|
216
217
|
try {
|
|
217
218
|
const response = await this.fetchApi(
|
|
218
|
-
"/api/v1/evaluate",
|
|
219
|
-
{
|
|
219
|
+
"/api/v1/evaluate/batch",
|
|
220
|
+
{ flags: this.flags, context: this.context }
|
|
220
221
|
);
|
|
221
|
-
|
|
222
|
-
this.cache.set(event.key, response.value);
|
|
223
|
-
if (oldValue !== response.value) {
|
|
224
|
-
this.emit("change", event.key, response.value);
|
|
225
|
-
}
|
|
222
|
+
this.applyBatchResult(response);
|
|
226
223
|
} catch {
|
|
227
224
|
}
|
|
228
225
|
}
|
|
@@ -279,4 +276,4 @@ var FlaggyClient = class {
|
|
|
279
276
|
export {
|
|
280
277
|
FlaggyClient
|
|
281
278
|
};
|
|
282
|
-
//# sourceMappingURL=chunk-
|
|
279
|
+
//# sourceMappingURL=chunk-K665S5QT.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sse.ts","../src/client.ts"],"sourcesContent":["import type { SSEEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: SSEEvent) => 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: SSEEvent) => 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 (this.isAbortError(err)) 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 (this.isAbortError(err)) 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 Record<string, unknown>;\n // SSE \"event:\" field is the authoritative event type\n if (eventType) {\n parsed.type = eventType;\n }\n this.onEvent(parsed as SSEEvent);\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 /** Safari/WebKit throws TypeError instead of AbortError when a fetch is aborted */\n private isAbortError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n if (err.name === 'AbortError') return true;\n if (err.name === 'TypeError' && /load failed|cancelled/i.test(err.message)) return true;\n return false;\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 BatchEvaluateResponse,\n SSEEvent,\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: SSEEvent): Promise<void> {\n // Ignore connection confirmation\n if (event.type === 'connected') return;\n\n // Any flag/rule/segment change — re-evaluate all flags via batch\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 } catch {\n // Failed to re-evaluate, keep previous cached values\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
|
@@ -58,7 +58,7 @@ var SSEManager = class {
|
|
|
58
58
|
this.readStream(response.body);
|
|
59
59
|
}).catch((err) => {
|
|
60
60
|
if (this.destroyed) return;
|
|
61
|
-
if (err
|
|
61
|
+
if (this.isAbortError(err)) return;
|
|
62
62
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
63
63
|
this.reconnect();
|
|
64
64
|
});
|
|
@@ -101,7 +101,7 @@ var SSEManager = class {
|
|
|
101
101
|
}
|
|
102
102
|
} catch (err) {
|
|
103
103
|
if (this.destroyed) return;
|
|
104
|
-
if (err
|
|
104
|
+
if (this.isAbortError(err)) return;
|
|
105
105
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
106
106
|
} finally {
|
|
107
107
|
reader.releaseLock();
|
|
@@ -113,7 +113,7 @@ var SSEManager = class {
|
|
|
113
113
|
handleEvent(eventType, data) {
|
|
114
114
|
try {
|
|
115
115
|
const parsed = JSON.parse(data);
|
|
116
|
-
if (eventType
|
|
116
|
+
if (eventType) {
|
|
117
117
|
parsed.type = eventType;
|
|
118
118
|
}
|
|
119
119
|
this.onEvent(parsed);
|
|
@@ -129,6 +129,13 @@ var SSEManager = class {
|
|
|
129
129
|
this.connect();
|
|
130
130
|
}, delay);
|
|
131
131
|
}
|
|
132
|
+
/** Safari/WebKit throws TypeError instead of AbortError when a fetch is aborted */
|
|
133
|
+
isAbortError(err) {
|
|
134
|
+
if (!(err instanceof Error)) return false;
|
|
135
|
+
if (err.name === "AbortError") return true;
|
|
136
|
+
if (err.name === "TypeError" && /load failed|cancelled/i.test(err.message)) return true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
132
139
|
getBackoffDelay() {
|
|
133
140
|
const delay = this.retryDelay * Math.pow(2, this.retryCount);
|
|
134
141
|
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
@@ -232,23 +239,13 @@ var FlaggyClient = class {
|
|
|
232
239
|
this.sseManager.connect();
|
|
233
240
|
}
|
|
234
241
|
async handleSSEEvent(event) {
|
|
235
|
-
if (event.type === "
|
|
236
|
-
if (this.cache.has(event.key)) {
|
|
237
|
-
this.cache.delete(event.key);
|
|
238
|
-
this.emit("change", event.key, void 0);
|
|
239
|
-
}
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
+
if (event.type === "connected") return;
|
|
242
243
|
try {
|
|
243
244
|
const response = await this.fetchApi(
|
|
244
|
-
"/api/v1/evaluate",
|
|
245
|
-
{
|
|
245
|
+
"/api/v1/evaluate/batch",
|
|
246
|
+
{ flags: this.flags, context: this.context }
|
|
246
247
|
);
|
|
247
|
-
|
|
248
|
-
this.cache.set(event.key, response.value);
|
|
249
|
-
if (oldValue !== response.value) {
|
|
250
|
-
this.emit("change", event.key, response.value);
|
|
251
|
-
}
|
|
248
|
+
this.applyBatchResult(response);
|
|
252
249
|
} catch {
|
|
253
250
|
}
|
|
254
251
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 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":[]}
|
|
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 SSEEventType,\n SSEEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n","import type { SSEEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: SSEEvent) => 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: SSEEvent) => 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 (this.isAbortError(err)) 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 (this.isAbortError(err)) 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 Record<string, unknown>;\n // SSE \"event:\" field is the authoritative event type\n if (eventType) {\n parsed.type = eventType;\n }\n this.onEvent(parsed as SSEEvent);\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 /** Safari/WebKit throws TypeError instead of AbortError when a fetch is aborted */\n private isAbortError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n if (err.name === 'AbortError') return true;\n if (err.name === 'TypeError' && /load failed|cancelled/i.test(err.message)) return true;\n return false;\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 BatchEvaluateResponse,\n SSEEvent,\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: SSEEvent): Promise<void> {\n // Ignore connection confirmation\n if (event.type === 'connected') return;\n\n // Any flag/rule/segment change — re-evaluate all flags via batch\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 } catch {\n // Failed to re-evaluate, keep previous cached values\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
|
@@ -20,10 +20,12 @@ interface EvaluateResponse {
|
|
|
20
20
|
interface BatchEvaluateResponse {
|
|
21
21
|
results: EvaluatedFlag[];
|
|
22
22
|
}
|
|
23
|
-
/** SSE event
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/** SSE event types sent by the server */
|
|
24
|
+
type SSEEventType = 'connected' | 'flag_created' | 'flag_updated' | 'flag_deleted' | 'flag_toggled' | 'rule_created' | 'rule_updated' | 'rule_deleted' | 'segment_created' | 'segment_updated' | 'segment_deleted';
|
|
25
|
+
/** SSE event data from the stream */
|
|
26
|
+
interface SSEEvent {
|
|
27
|
+
type: SSEEventType;
|
|
28
|
+
[key: string]: unknown;
|
|
27
29
|
}
|
|
28
30
|
/** Configuration for FlaggyClient */
|
|
29
31
|
interface FlaggyClientOptions {
|
|
@@ -80,4 +82,4 @@ declare class FlaggyClient {
|
|
|
80
82
|
private fetchApi;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
export { type BatchEvaluateResponse, type ErrorListener, type EvaluateResponse, type EvaluatedFlag, type
|
|
85
|
+
export { type BatchEvaluateResponse, type ErrorListener, type EvaluateResponse, type EvaluatedFlag, type FlagChangeListener, type FlagValue, FlaggyClient, type FlaggyClientOptions, type FlaggyContext, type ReadyListener, type SSEEvent, type SSEEventType };
|
package/dist/index.d.ts
CHANGED
|
@@ -20,10 +20,12 @@ interface EvaluateResponse {
|
|
|
20
20
|
interface BatchEvaluateResponse {
|
|
21
21
|
results: EvaluatedFlag[];
|
|
22
22
|
}
|
|
23
|
-
/** SSE event
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/** SSE event types sent by the server */
|
|
24
|
+
type SSEEventType = 'connected' | 'flag_created' | 'flag_updated' | 'flag_deleted' | 'flag_toggled' | 'rule_created' | 'rule_updated' | 'rule_deleted' | 'segment_created' | 'segment_updated' | 'segment_deleted';
|
|
25
|
+
/** SSE event data from the stream */
|
|
26
|
+
interface SSEEvent {
|
|
27
|
+
type: SSEEventType;
|
|
28
|
+
[key: string]: unknown;
|
|
27
29
|
}
|
|
28
30
|
/** Configuration for FlaggyClient */
|
|
29
31
|
interface FlaggyClientOptions {
|
|
@@ -80,4 +82,4 @@ declare class FlaggyClient {
|
|
|
80
82
|
private fetchApi;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
export { type BatchEvaluateResponse, type ErrorListener, type EvaluateResponse, type EvaluatedFlag, type
|
|
85
|
+
export { type BatchEvaluateResponse, type ErrorListener, type EvaluateResponse, type EvaluatedFlag, type FlagChangeListener, type FlagValue, FlaggyClient, type FlaggyClientOptions, type FlaggyContext, type ReadyListener, type SSEEvent, type SSEEventType };
|
package/dist/index.mjs
CHANGED
package/dist/react.cjs
CHANGED
|
@@ -63,7 +63,7 @@ var SSEManager = class {
|
|
|
63
63
|
this.readStream(response.body);
|
|
64
64
|
}).catch((err) => {
|
|
65
65
|
if (this.destroyed) return;
|
|
66
|
-
if (err
|
|
66
|
+
if (this.isAbortError(err)) return;
|
|
67
67
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
68
68
|
this.reconnect();
|
|
69
69
|
});
|
|
@@ -106,7 +106,7 @@ var SSEManager = class {
|
|
|
106
106
|
}
|
|
107
107
|
} catch (err) {
|
|
108
108
|
if (this.destroyed) return;
|
|
109
|
-
if (err
|
|
109
|
+
if (this.isAbortError(err)) return;
|
|
110
110
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
111
111
|
} finally {
|
|
112
112
|
reader.releaseLock();
|
|
@@ -118,7 +118,7 @@ var SSEManager = class {
|
|
|
118
118
|
handleEvent(eventType, data) {
|
|
119
119
|
try {
|
|
120
120
|
const parsed = JSON.parse(data);
|
|
121
|
-
if (eventType
|
|
121
|
+
if (eventType) {
|
|
122
122
|
parsed.type = eventType;
|
|
123
123
|
}
|
|
124
124
|
this.onEvent(parsed);
|
|
@@ -134,6 +134,13 @@ var SSEManager = class {
|
|
|
134
134
|
this.connect();
|
|
135
135
|
}, delay);
|
|
136
136
|
}
|
|
137
|
+
/** Safari/WebKit throws TypeError instead of AbortError when a fetch is aborted */
|
|
138
|
+
isAbortError(err) {
|
|
139
|
+
if (!(err instanceof Error)) return false;
|
|
140
|
+
if (err.name === "AbortError") return true;
|
|
141
|
+
if (err.name === "TypeError" && /load failed|cancelled/i.test(err.message)) return true;
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
137
144
|
getBackoffDelay() {
|
|
138
145
|
const delay = this.retryDelay * Math.pow(2, this.retryCount);
|
|
139
146
|
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
@@ -237,23 +244,13 @@ var FlaggyClient = class {
|
|
|
237
244
|
this.sseManager.connect();
|
|
238
245
|
}
|
|
239
246
|
async handleSSEEvent(event) {
|
|
240
|
-
if (event.type === "
|
|
241
|
-
if (this.cache.has(event.key)) {
|
|
242
|
-
this.cache.delete(event.key);
|
|
243
|
-
this.emit("change", event.key, void 0);
|
|
244
|
-
}
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
+
if (event.type === "connected") return;
|
|
247
248
|
try {
|
|
248
249
|
const response = await this.fetchApi(
|
|
249
|
-
"/api/v1/evaluate",
|
|
250
|
-
{
|
|
250
|
+
"/api/v1/evaluate/batch",
|
|
251
|
+
{ flags: this.flags, context: this.context }
|
|
251
252
|
);
|
|
252
|
-
|
|
253
|
-
this.cache.set(event.key, response.value);
|
|
254
|
-
if (oldValue !== response.value) {
|
|
255
|
-
this.emit("change", event.key, response.value);
|
|
256
|
-
}
|
|
253
|
+
this.applyBatchResult(response);
|
|
257
254
|
} catch {
|
|
258
255
|
}
|
|
259
256
|
}
|
|
@@ -325,7 +322,7 @@ function FlaggyProvider({
|
|
|
325
322
|
const clientRef = (0, import_react2.useRef)(null);
|
|
326
323
|
const [ready, setReady] = (0, import_react2.useState)(false);
|
|
327
324
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
328
|
-
const [, setVersion] = (0, import_react2.useState)(0);
|
|
325
|
+
const [version, setVersion] = (0, import_react2.useState)(0);
|
|
329
326
|
(0, import_react2.useEffect)(() => {
|
|
330
327
|
const client = new FlaggyClient({
|
|
331
328
|
serverUrl,
|
|
@@ -339,7 +336,7 @@ function FlaggyProvider({
|
|
|
339
336
|
setError(null);
|
|
340
337
|
const unsubReady = client.on("ready", () => setReady(true));
|
|
341
338
|
const unsubError = client.on("error", (err) => {
|
|
342
|
-
setError(err);
|
|
339
|
+
if (!client.ready) setError(err);
|
|
343
340
|
onError?.(err);
|
|
344
341
|
});
|
|
345
342
|
const unsubChange = client.on("change", () => {
|
|
@@ -362,7 +359,7 @@ function FlaggyProvider({
|
|
|
362
359
|
}, [contextKey]);
|
|
363
360
|
const value = (0, import_react2.useMemo)(
|
|
364
361
|
() => clientRef.current ? { client: clientRef.current, ready, error } : null,
|
|
365
|
-
[ready, error]
|
|
362
|
+
[ready, error, version]
|
|
366
363
|
);
|
|
367
364
|
if (!value) return null;
|
|
368
365
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlaggyReactContext.Provider, { value, children });
|
package/dist/react.cjs.map
CHANGED
|
@@ -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 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":"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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 [version, 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 // Only set error state during init; SSE errors are transient\n if (!client.ready) 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, version],\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 { SSEEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: SSEEvent) => 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: SSEEvent) => 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 (this.isAbortError(err)) 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 (this.isAbortError(err)) 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 Record<string, unknown>;\n // SSE \"event:\" field is the authoritative event type\n if (eventType) {\n parsed.type = eventType;\n }\n this.onEvent(parsed as SSEEvent);\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 /** Safari/WebKit throws TypeError instead of AbortError when a fetch is aborted */\n private isAbortError(err: unknown): boolean {\n if (!(err instanceof Error)) return false;\n if (err.name === 'AbortError') return true;\n if (err.name === 'TypeError' && /load failed|cancelled/i.test(err.message)) return true;\n return false;\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 BatchEvaluateResponse,\n SSEEvent,\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: SSEEvent): Promise<void> {\n // Ignore connection confirmation\n if (event.type === 'connected') return;\n\n // Any flag/rule/segment change — re-evaluate all flags via batch\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 } catch {\n // Failed to re-evaluate, keep previous cached values\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"]}
|
package/dist/react.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FlaggyClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-K665S5QT.mjs";
|
|
4
4
|
|
|
5
5
|
// src/react/FlaggyProvider.tsx
|
|
6
6
|
import { useEffect, useRef, useState, useMemo } from "react";
|
|
@@ -23,7 +23,7 @@ function FlaggyProvider({
|
|
|
23
23
|
const clientRef = useRef(null);
|
|
24
24
|
const [ready, setReady] = useState(false);
|
|
25
25
|
const [error, setError] = useState(null);
|
|
26
|
-
const [, setVersion] = useState(0);
|
|
26
|
+
const [version, setVersion] = useState(0);
|
|
27
27
|
useEffect(() => {
|
|
28
28
|
const client = new FlaggyClient({
|
|
29
29
|
serverUrl,
|
|
@@ -37,7 +37,7 @@ function FlaggyProvider({
|
|
|
37
37
|
setError(null);
|
|
38
38
|
const unsubReady = client.on("ready", () => setReady(true));
|
|
39
39
|
const unsubError = client.on("error", (err) => {
|
|
40
|
-
setError(err);
|
|
40
|
+
if (!client.ready) setError(err);
|
|
41
41
|
onError?.(err);
|
|
42
42
|
});
|
|
43
43
|
const unsubChange = client.on("change", () => {
|
|
@@ -60,7 +60,7 @@ function FlaggyProvider({
|
|
|
60
60
|
}, [contextKey]);
|
|
61
61
|
const value = useMemo(
|
|
62
62
|
() => clientRef.current ? { client: clientRef.current, ready, error } : null,
|
|
63
|
-
[ready, error]
|
|
63
|
+
[ready, error, version]
|
|
64
64
|
);
|
|
65
65
|
if (!value) return null;
|
|
66
66
|
return /* @__PURE__ */ jsx(FlaggyReactContext.Provider, { value, children });
|
package/dist/react.mjs.map
CHANGED
|
@@ -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 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;;;
|
|
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 [version, 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 // Only set error state during init; SSE errors are transient\n if (!client.ready) 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, version],\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;;;AD0E3E;AAnEG,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,SAAS,UAAU,IAAI,SAAS,CAAC;AAGxC,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;AAE7C,UAAI,CAAC,OAAO,MAAO,UAAS,GAAG;AAC/B,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,OAAO,OAAO;AAAA,EACxB;AAEA,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,oBAAC,mBAAmB,UAAnB,EAA4B,OAC1B,UACH;AAEJ;;;AEvFA,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 +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 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":[]}
|