@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.
@@ -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 instanceof Error && err.name === "AbortError") return;
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 instanceof Error && err.name === "AbortError") return;
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 && !parsed.type) {
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 === "flag_deleted") {
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
- { flag_key: event.key, context: this.context }
219
+ "/api/v1/evaluate/batch",
220
+ { flags: this.flags, context: this.context }
220
221
  );
221
- const oldValue = this.cache.get(event.key);
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-YLOZXW2K.mjs.map
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":";AAWO,IAAM,aAAN,MAAiB;AAAA,EAatB,YAAY,SAA4B;AAZxC,SAAQ,kBAA0C;AAClD,SAAQ,aAAa;AACrB,SAAQ,eAAqD;AAC7D,SAAQ,YAAY;AAUlB,SAAK,MAAM,QAAQ;AACnB,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AACvB,SAAK,UAAU,QAAQ;AACvB,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,gBAAgB,QAAQ,iBAAiB;AAAA,EAChD;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,UAAW;AAEpB,SAAK,kBAAkB,IAAI,gBAAgB;AAE3C,UAAM,KAAK,KAAK;AAAA,MACd,SAAS;AAAA,QACP,eAAe,UAAU,KAAK,MAAM;AAAA,QACpC,QAAQ;AAAA,MACV;AAAA,MACA,QAAQ,KAAK,gBAAgB;AAAA,IAC/B,CAAC,EACE,KAAK,CAAC,aAAa;AAClB,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,EAAE;AAAA,MAC7D;AACA,UAAI,CAAC,SAAS,MAAM;AAClB,cAAM,IAAI,MAAM,0BAA0B;AAAA,MAC5C;AAEA,WAAK,aAAa;AAClB,WAAK,WAAW,SAAS,IAAI;AAAA,IAC/B,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,UAAI,KAAK,UAAW;AACpB,UAAI,KAAK,aAAa,GAAG,EAAG;AAE5B,WAAK,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAChE,WAAK,UAAU;AAAA,IACjB,CAAC;AAAA,EACL;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY;AACjB,SAAK,iBAAiB,MAAM;AAC5B,SAAK,kBAAkB;AACvB,QAAI,KAAK,cAAc;AACrB,mBAAa,KAAK,YAAY;AAC9B,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAc,WAAW,MAAiD;AACxE,UAAM,SAAS,KAAK,UAAU;AAC9B,UAAM,UAAU,IAAI,YAAY;AAChC,QAAI,SAAS;AACb,QAAI,eAAe;AACnB,QAAI,cAAc;AAElB,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,YAAI,KAAM;AAEV,kBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAEhD,cAAM,QAAQ,OAAO,MAAM,IAAI;AAE/B,iBAAS,MAAM,IAAI,KAAK;AAExB,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,WAAW,QAAQ,GAAG;AAC7B,2BAAe,KAAK,MAAM,CAAC,EAAE,KAAK;AAAA,UACpC,WAAW,KAAK,WAAW,OAAO,GAAG;AACnC,0BAAc,KAAK,MAAM,CAAC,EAAE,KAAK;AAAA,UACnC,WAAW,SAAS,IAAI;AAEtB,gBAAI,aAAa;AACf,mBAAK,YAAY,cAAc,WAAW;AAAA,YAC5C;AACA,2BAAe;AACf,0BAAc;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,KAAc;AACrB,UAAI,KAAK,UAAW;AACpB,UAAI,KAAK,aAAa,GAAG,EAAG;AAE5B,WAAK,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAClE,UAAE;AACA,aAAO,YAAY;AAAA,IACrB;AAGA,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAmB,MAAoB;AACzD,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,IAAI;AAE9B,UAAI,WAAW;AACb,eAAO,OAAO;AAAA,MAChB;AACA,WAAK,QAAQ,MAAkB;AAAA,IACjC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,YAAkB;AACxB,QAAI,KAAK,UAAW;AAEpB,UAAM,QAAQ,KAAK,gBAAgB;AACnC,SAAK;AACL,SAAK,eAAe,WAAW,MAAM;AACnC,WAAK,eAAe;AACpB,WAAK,QAAQ;AAAA,IACf,GAAG,KAAK;AAAA,EACV;AAAA;AAAA,EAGQ,aAAa,KAAuB;AAC1C,QAAI,EAAE,eAAe,OAAQ,QAAO;AACpC,QAAI,IAAI,SAAS,aAAc,QAAO;AACtC,QAAI,IAAI,SAAS,eAAe,yBAAyB,KAAK,IAAI,OAAO,EAAG,QAAO;AACnF,WAAO;AAAA,EACT;AAAA,EAEQ,kBAA0B;AAChC,UAAM,QAAQ,KAAK,aAAa,KAAK,IAAI,GAAG,KAAK,UAAU;AAC3D,UAAM,SAAS,QAAQ,QAAQ,KAAK,OAAO,IAAI,IAAI;AACnD,WAAO,KAAK,IAAI,QAAQ,QAAQ,KAAK,aAAa;AAAA,EACpD;AACF;;;AC9IO,IAAM,eAAN,MAAmB;AAAA,EAyBxB,YAAY,SAA8B;AAhB1C,SAAQ,QAAQ,oBAAI,IAAuB;AAC3C,SAAQ,SAAS;AACjB,SAAQ,SAAuB;AAC/B,SAAQ,aAAgC;AACxC,SAAQ,yBAAiD;AAEzD,SAAQ,YAIJ;AAAA,MACF,QAAQ,oBAAI,IAAI;AAAA,MAChB,OAAO,oBAAI,IAAI;AAAA,MACf,OAAO,oBAAI,IAAI;AAAA,IACjB;AAGE,SAAK,YAAY,QAAQ,UAAU,QAAQ,OAAO,EAAE;AACpD,SAAK,SAAS,QAAQ;AACtB,SAAK,QAAQ,QAAQ;AACrB,SAAK,UAAU,QAAQ,WAAW,CAAC;AACnC,SAAK,kBAAkB,QAAQ,mBAAmB;AAClD,SAAK,gBAAgB,QAAQ,iBAAiB;AAC9C,SAAK,mBAAmB,QAAQ,oBAAoB;AAAA,EACtD;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,QAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,aAA4B;AAChC,QAAI;AACF,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,QACA,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,QAAQ;AAAA,MAC7C;AACA,WAAK,iBAAiB,QAAQ;AAC9B,WAAK,SAAS;AACd,WAAK,KAAK,OAAO;AAAA,IACnB,SAAS,KAAc;AACrB,WAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,WAAK,KAAK,SAAS,KAAK,MAAM;AAAA,IAChC;AAEA,QAAI,KAAK,iBAAiB;AACxB,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,QAA6B,KAAa,cAAoB;AAC5D,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,MAAM,IAAI,GAAG,GAAG;AACxC,aAAO;AAAA,IACT;AACA,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EAEA,MAAM,WAAW,SAAuC;AACtD,SAAK,UAAU;AACf,SAAK,wBAAwB,MAAM;AACnC,UAAM,aAAa,IAAI,gBAAgB;AACvC,SAAK,yBAAyB;AAE9B,QAAI;AACF,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,QACA,EAAE,OAAO,KAAK,OAAO,QAAQ;AAAA,QAC7B,WAAW;AAAA,MACb;AACA,UAAI,WAAW,OAAO,QAAS;AAC/B,WAAK,iBAAiB,QAAQ;AAAA,IAChC,SAAS,KAAc;AACrB,UAAI,eAAe,SAAS,IAAI,SAAS,aAAc;AACvD,WAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,WAAK,KAAK,SAAS,KAAK,MAAM;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,GAA6B,OAAU,UAAmC;AACxE,IAAC,KAAK,UAAU,KAAK,EAAuB,IAAI,QAAQ;AACxD,WAAO,MAAM;AACX,MAAC,KAAK,UAAU,KAAK,EAAuB,OAAO,QAAQ;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY,QAAQ;AACzB,SAAK,aAAa;AAClB,SAAK,wBAAwB,MAAM;AACnC,SAAK,yBAAyB;AAC9B,SAAK,UAAU,OAAO,MAAM;AAC5B,SAAK,UAAU,MAAM,MAAM;AAC3B,SAAK,UAAU,MAAM,MAAM;AAAA,EAC7B;AAAA,EAEQ,WAAiB;AACvB,SAAK,aAAa,IAAI,WAAW;AAAA,MAC/B,KAAK,GAAG,KAAK,SAAS;AAAA,MACtB,QAAQ,KAAK;AAAA,MACb,SAAS,CAAC,UAAU,KAAK,eAAe,KAAK;AAAA,MAC7C,SAAS,CAAC,QAAQ,KAAK,KAAK,SAAS,GAAG;AAAA,MACxC,YAAY,KAAK;AAAA,MACjB,eAAe,KAAK;AAAA,IACtB,CAAC;AACD,SAAK,WAAW,QAAQ;AAAA,EAC1B;AAAA,EAEA,MAAc,eAAe,OAAgC;AAE3D,QAAI,MAAM,SAAS,YAAa;AAGhC,QAAI;AACF,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B;AAAA,QACA,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,QAAQ;AAAA,MAC7C;AACA,WAAK,iBAAiB,QAAQ;AAAA,IAChC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,iBAAiB,UAAuC;AAC9D,UAAM,WAAW,oBAAI,IAAuB;AAC5C,eAAW,QAAQ,SAAS,SAAS;AACnC,eAAS,IAAI,KAAK,UAAU,KAAK,KAAK;AAAA,IACxC;AAGA,eAAW,CAAC,KAAK,QAAQ,KAAK,UAAU;AACtC,YAAM,WAAW,KAAK,MAAM,IAAI,GAAG;AACnC,UAAI,aAAa,UAAU;AACzB,aAAK,KAAK,UAAU,KAAK,QAAQ;AAAA,MACnC;AAAA,IACF;AAGA,eAAW,OAAO,KAAK,MAAM,KAAK,GAAG;AACnC,UAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AACtB,aAAK,KAAK,UAAU,KAAK,MAAiC;AAAA,MAC5D;AAAA,IACF;AAEA,SAAK,QAAQ;AAAA,EACf;AAAA,EAKQ,KAAK,UAA0B,MAAuB;AAC5D,QAAI,UAAU,UAAU;AACtB,iBAAW,YAAY,KAAK,UAAU,QAAQ;AAC5C,iBAAS,KAAK,CAAC,GAAa,KAAK,CAAC,CAAc;AAAA,MAClD;AAAA,IACF,WAAW,UAAU,SAAS;AAC5B,iBAAW,YAAY,KAAK,UAAU,OAAO;AAC3C,iBAAS;AAAA,MACX;AAAA,IACF,WAAW,UAAU,SAAS;AAC5B,iBAAW,YAAY,KAAK,UAAU,OAAO;AAC3C,iBAAS,KAAK,CAAC,CAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,SACZ,MACA,MACA,QACY;AACZ,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,SAAS,GAAG,IAAI,IAAI;AAAA,MACvD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,MAAM;AAAA,MACtC;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB;AAAA,IACF,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,qBAAqB,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,IAC/E;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AACF;","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 instanceof Error && err.name === "AbortError") return;
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 instanceof Error && err.name === "AbortError") return;
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 && !parsed.type) {
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 === "flag_deleted") {
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
- { flag_key: event.key, context: this.context }
245
+ "/api/v1/evaluate/batch",
246
+ { flags: this.flags, context: this.context }
246
247
  );
247
- const oldValue = this.cache.get(event.key);
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
  }
@@ -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 data for flag changes */
24
- interface FlagChangeEvent {
25
- type: 'flag_updated' | 'flag_deleted' | 'flag_created';
26
- key: string;
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 FlagChangeEvent, type FlagChangeListener, type FlagValue, FlaggyClient, type FlaggyClientOptions, type FlaggyContext, type ReadyListener };
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 data for flag changes */
24
- interface FlagChangeEvent {
25
- type: 'flag_updated' | 'flag_deleted' | 'flag_created';
26
- key: string;
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 FlagChangeEvent, type FlagChangeListener, type FlagValue, FlaggyClient, type FlaggyClientOptions, type FlaggyContext, type ReadyListener };
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FlaggyClient
3
- } from "./chunk-YLOZXW2K.mjs";
3
+ } from "./chunk-K665S5QT.mjs";
4
4
  export {
5
5
  FlaggyClient
6
6
  };
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 instanceof Error && err.name === "AbortError") return;
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 instanceof Error && err.name === "AbortError") return;
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 && !parsed.type) {
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 === "flag_deleted") {
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
- { flag_key: event.key, context: this.context }
250
+ "/api/v1/evaluate/batch",
251
+ { flags: this.flags, context: this.context }
251
252
  );
252
- const oldValue = this.cache.get(event.key);
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 });
@@ -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-YLOZXW2K.mjs";
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 });
@@ -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;;;ADyE3E;AAlEG,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,YAAY,OAA4B,IAAI;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,EAAE,UAAU,IAAI,SAAS,CAAC;AAGjC,YAAU,MAAM;AACd,UAAM,SAAS,IAAI,aAAa;AAAA,MAC9B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,cAAU,UAAU;AACpB,aAAS,KAAK;AACd,aAAS,IAAI;AAEb,UAAM,aAAa,OAAO,GAAG,SAAS,MAAM,SAAS,IAAI,CAAC;AAC1D,UAAM,aAAa,OAAO,GAAG,SAAS,CAAC,QAAQ;AAC7C,eAAS,GAAG;AACZ,gBAAU,GAAG;AAAA,IACf,CAAC;AACD,UAAM,cAAc,OAAO,GAAG,UAAU,MAAM;AAC5C,iBAAW,CAAC,MAAM,IAAI,CAAC;AAAA,IACzB,CAAC;AAED,WAAO,WAAW;AAElB,WAAO,MAAM;AACX,iBAAW;AACX,iBAAW;AACX,kBAAY;AACZ,aAAO,QAAQ;AACf,gBAAU,UAAU;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,WAAW,MAAM,CAAC;AAGtB,QAAM,aAAa,UAAU,KAAK,UAAU,OAAO,IAAI;AACvD,YAAU,MAAM;AACd,QAAI,UAAU,WAAW,WAAW,UAAU,QAAQ,OAAO;AAC3D,gBAAU,QAAQ,WAAW,OAAO;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,QAAQ;AAAA,IACZ,MACE,UAAU,UACN,EAAE,QAAQ,UAAU,SAAS,OAAO,MAAM,IAC1C;AAAA,IACN,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,MAAI,CAAC,MAAO,QAAO;AAEnB,SACE,oBAAC,mBAAmB,UAAnB,EAA4B,OAC1B,UACH;AAEJ;;;AEtFA,SAAS,kBAAkB;AAIpB,SAAS,QAA6B,KAAa,cAAoB;AAC5E,QAAM,MAAM,WAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,SAAO,IAAI,OAAO,QAAQ,KAAK,YAAY;AAC7C;;;ACZA,SAAS,cAAAA,mBAAkB;AAGpB,SAAS,YAAY;AAC1B,QAAM,MAAMC,YAAW,kBAAkB;AAEzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF;AAEA,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,OAAO,IAAI;AAAA,EACb;AACF;","names":["useContext","useContext"]}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@getflaggy/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "JavaScript/React SDK for the Flaggy feature flag server",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/sse.ts","../src/client.ts"],"sourcesContent":["import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly 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":[]}