@getflaggy/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,281 @@
1
+ // src/sse.ts
2
+ var SSEManager = class {
3
+ constructor(options) {
4
+ this.abortController = null;
5
+ this.retryCount = 0;
6
+ this.retryTimeout = null;
7
+ this.destroyed = false;
8
+ this.url = options.url;
9
+ this.apiKey = options.apiKey;
10
+ this.onEvent = options.onEvent;
11
+ this.onError = options.onError;
12
+ this.retryDelay = options.retryDelay ?? 1e3;
13
+ this.maxRetryDelay = options.maxRetryDelay ?? 3e4;
14
+ }
15
+ connect() {
16
+ if (this.destroyed) return;
17
+ this.abortController = new AbortController();
18
+ fetch(this.url, {
19
+ headers: {
20
+ Authorization: `Bearer ${this.apiKey}`,
21
+ Accept: "text/event-stream"
22
+ },
23
+ signal: this.abortController.signal
24
+ }).then((response) => {
25
+ if (!response.ok) {
26
+ throw new Error(`SSE connection failed: ${response.status}`);
27
+ }
28
+ if (!response.body) {
29
+ throw new Error("SSE response has no body");
30
+ }
31
+ this.retryCount = 0;
32
+ this.readStream(response.body);
33
+ }).catch((err) => {
34
+ if (this.destroyed) return;
35
+ if (err instanceof Error && err.name === "AbortError") return;
36
+ this.onError(err instanceof Error ? err : new Error(String(err)));
37
+ this.reconnect();
38
+ });
39
+ }
40
+ destroy() {
41
+ this.destroyed = true;
42
+ this.abortController?.abort();
43
+ this.abortController = null;
44
+ if (this.retryTimeout) {
45
+ clearTimeout(this.retryTimeout);
46
+ this.retryTimeout = null;
47
+ }
48
+ }
49
+ async readStream(body) {
50
+ const reader = body.getReader();
51
+ const decoder = new TextDecoder();
52
+ let buffer = "";
53
+ let currentEvent = "";
54
+ let currentData = "";
55
+ try {
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) break;
59
+ buffer += decoder.decode(value, { stream: true });
60
+ const lines = buffer.split("\n");
61
+ buffer = lines.pop() ?? "";
62
+ for (const line of lines) {
63
+ if (line.startsWith("event:")) {
64
+ currentEvent = line.slice(6).trim();
65
+ } else if (line.startsWith("data:")) {
66
+ currentData = line.slice(5).trim();
67
+ } else if (line === "") {
68
+ if (currentData) {
69
+ this.handleEvent(currentEvent, currentData);
70
+ }
71
+ currentEvent = "";
72
+ currentData = "";
73
+ }
74
+ }
75
+ }
76
+ } catch (err) {
77
+ if (this.destroyed) return;
78
+ if (err instanceof Error && err.name === "AbortError") return;
79
+ this.onError(err instanceof Error ? err : new Error(String(err)));
80
+ } finally {
81
+ reader.releaseLock();
82
+ }
83
+ if (!this.destroyed) {
84
+ this.reconnect();
85
+ }
86
+ }
87
+ handleEvent(eventType, data) {
88
+ try {
89
+ const parsed = JSON.parse(data);
90
+ if (eventType && !parsed.type) {
91
+ parsed.type = eventType;
92
+ }
93
+ this.onEvent(parsed);
94
+ } catch {
95
+ }
96
+ }
97
+ reconnect() {
98
+ if (this.destroyed) return;
99
+ const delay = this.getBackoffDelay();
100
+ this.retryCount++;
101
+ this.retryTimeout = setTimeout(() => {
102
+ this.retryTimeout = null;
103
+ this.connect();
104
+ }, delay);
105
+ }
106
+ getBackoffDelay() {
107
+ const delay = this.retryDelay * Math.pow(2, this.retryCount);
108
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
109
+ return Math.min(delay + jitter, this.maxRetryDelay);
110
+ }
111
+ };
112
+
113
+ // src/client.ts
114
+ var FlaggyClient = class {
115
+ constructor(options) {
116
+ this.cache = /* @__PURE__ */ new Map();
117
+ this._ready = false;
118
+ this._error = null;
119
+ this.sseManager = null;
120
+ this.contextAbortController = null;
121
+ this.listeners = {
122
+ change: /* @__PURE__ */ new Set(),
123
+ ready: /* @__PURE__ */ new Set(),
124
+ error: /* @__PURE__ */ new Set()
125
+ };
126
+ this.serverUrl = options.serverUrl.replace(/\/$/, "");
127
+ this.apiKey = options.apiKey;
128
+ this.context = options.context ?? {};
129
+ this.enableStreaming = options.enableStreaming ?? true;
130
+ this.sseRetryDelay = options.sseRetryDelay ?? 1e3;
131
+ this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 3e4;
132
+ }
133
+ get ready() {
134
+ return this._ready;
135
+ }
136
+ get error() {
137
+ return this._error;
138
+ }
139
+ async initialize() {
140
+ try {
141
+ const response = await this.fetchApi(
142
+ "/api/v1/evaluate/batch",
143
+ { context: this.context }
144
+ );
145
+ this.applyBatchResult(response);
146
+ this._ready = true;
147
+ this.emit("ready");
148
+ } catch (err) {
149
+ this._error = err instanceof Error ? err : new Error(String(err));
150
+ this.emit("error", this._error);
151
+ }
152
+ if (this.enableStreaming) {
153
+ this.startSSE();
154
+ }
155
+ }
156
+ getFlag(key, defaultValue) {
157
+ if (!this._ready || !this.cache.has(key)) {
158
+ return defaultValue;
159
+ }
160
+ return this.cache.get(key);
161
+ }
162
+ async setContext(context) {
163
+ this.context = context;
164
+ this.contextAbortController?.abort();
165
+ const controller = new AbortController();
166
+ this.contextAbortController = controller;
167
+ try {
168
+ const response = await this.fetchApi(
169
+ "/api/v1/evaluate/batch",
170
+ { context },
171
+ controller.signal
172
+ );
173
+ if (controller.signal.aborted) return;
174
+ this.applyBatchResult(response);
175
+ } catch (err) {
176
+ if (err instanceof Error && err.name === "AbortError") return;
177
+ this._error = err instanceof Error ? err : new Error(String(err));
178
+ this.emit("error", this._error);
179
+ }
180
+ }
181
+ on(event, listener) {
182
+ this.listeners[event].add(listener);
183
+ return () => {
184
+ this.listeners[event].delete(listener);
185
+ };
186
+ }
187
+ destroy() {
188
+ this.sseManager?.destroy();
189
+ this.sseManager = null;
190
+ this.contextAbortController?.abort();
191
+ this.contextAbortController = null;
192
+ this.listeners.change.clear();
193
+ this.listeners.ready.clear();
194
+ this.listeners.error.clear();
195
+ }
196
+ startSSE() {
197
+ this.sseManager = new SSEManager({
198
+ url: `${this.serverUrl}/api/v1/stream`,
199
+ apiKey: this.apiKey,
200
+ onEvent: (event) => this.handleSSEEvent(event),
201
+ onError: (err) => this.emit("error", err),
202
+ retryDelay: this.sseRetryDelay,
203
+ maxRetryDelay: this.sseMaxRetryDelay
204
+ });
205
+ this.sseManager.connect();
206
+ }
207
+ async handleSSEEvent(event) {
208
+ if (event.type === "flag_deleted") {
209
+ if (this.cache.has(event.key)) {
210
+ this.cache.delete(event.key);
211
+ this.emit("change", event.key, void 0);
212
+ }
213
+ return;
214
+ }
215
+ try {
216
+ const response = await this.fetchApi(
217
+ "/api/v1/evaluate",
218
+ { flag_key: event.key, context: this.context }
219
+ );
220
+ const oldValue = this.cache.get(event.key);
221
+ this.cache.set(event.key, response.value);
222
+ if (oldValue !== response.value) {
223
+ this.emit("change", event.key, response.value);
224
+ }
225
+ } catch {
226
+ }
227
+ }
228
+ applyBatchResult(response) {
229
+ const newCache = /* @__PURE__ */ new Map();
230
+ for (const flag of response.results) {
231
+ newCache.set(flag.flag_key, flag.value);
232
+ }
233
+ for (const [key, newValue] of newCache) {
234
+ const oldValue = this.cache.get(key);
235
+ if (oldValue !== newValue) {
236
+ this.emit("change", key, newValue);
237
+ }
238
+ }
239
+ for (const key of this.cache.keys()) {
240
+ if (!newCache.has(key)) {
241
+ this.emit("change", key, void 0);
242
+ }
243
+ }
244
+ this.cache = newCache;
245
+ }
246
+ emit(event, ...args) {
247
+ if (event === "change") {
248
+ for (const listener of this.listeners.change) {
249
+ listener(args[0], args[1]);
250
+ }
251
+ } else if (event === "ready") {
252
+ for (const listener of this.listeners.ready) {
253
+ listener();
254
+ }
255
+ } else if (event === "error") {
256
+ for (const listener of this.listeners.error) {
257
+ listener(args[0]);
258
+ }
259
+ }
260
+ }
261
+ async fetchApi(path, body, signal) {
262
+ const response = await fetch(`${this.serverUrl}${path}`, {
263
+ method: "POST",
264
+ headers: {
265
+ "Content-Type": "application/json",
266
+ Authorization: `Bearer ${this.apiKey}`
267
+ },
268
+ body: JSON.stringify(body),
269
+ signal
270
+ });
271
+ if (!response.ok) {
272
+ throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);
273
+ }
274
+ return response.json();
275
+ }
276
+ };
277
+
278
+ export {
279
+ FlaggyClient
280
+ };
281
+ //# sourceMappingURL=chunk-HBGNXCEV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sse.ts","../src/client.ts"],"sourcesContent":["import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ FlaggyClient: () => FlaggyClient
24
+ });
25
+ module.exports = __toCommonJS(src_exports);
26
+
27
+ // src/sse.ts
28
+ var SSEManager = class {
29
+ constructor(options) {
30
+ this.abortController = null;
31
+ this.retryCount = 0;
32
+ this.retryTimeout = null;
33
+ this.destroyed = false;
34
+ this.url = options.url;
35
+ this.apiKey = options.apiKey;
36
+ this.onEvent = options.onEvent;
37
+ this.onError = options.onError;
38
+ this.retryDelay = options.retryDelay ?? 1e3;
39
+ this.maxRetryDelay = options.maxRetryDelay ?? 3e4;
40
+ }
41
+ connect() {
42
+ if (this.destroyed) return;
43
+ this.abortController = new AbortController();
44
+ fetch(this.url, {
45
+ headers: {
46
+ Authorization: `Bearer ${this.apiKey}`,
47
+ Accept: "text/event-stream"
48
+ },
49
+ signal: this.abortController.signal
50
+ }).then((response) => {
51
+ if (!response.ok) {
52
+ throw new Error(`SSE connection failed: ${response.status}`);
53
+ }
54
+ if (!response.body) {
55
+ throw new Error("SSE response has no body");
56
+ }
57
+ this.retryCount = 0;
58
+ this.readStream(response.body);
59
+ }).catch((err) => {
60
+ if (this.destroyed) return;
61
+ if (err instanceof Error && err.name === "AbortError") return;
62
+ this.onError(err instanceof Error ? err : new Error(String(err)));
63
+ this.reconnect();
64
+ });
65
+ }
66
+ destroy() {
67
+ this.destroyed = true;
68
+ this.abortController?.abort();
69
+ this.abortController = null;
70
+ if (this.retryTimeout) {
71
+ clearTimeout(this.retryTimeout);
72
+ this.retryTimeout = null;
73
+ }
74
+ }
75
+ async readStream(body) {
76
+ const reader = body.getReader();
77
+ const decoder = new TextDecoder();
78
+ let buffer = "";
79
+ let currentEvent = "";
80
+ let currentData = "";
81
+ try {
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done) break;
85
+ buffer += decoder.decode(value, { stream: true });
86
+ const lines = buffer.split("\n");
87
+ buffer = lines.pop() ?? "";
88
+ for (const line of lines) {
89
+ if (line.startsWith("event:")) {
90
+ currentEvent = line.slice(6).trim();
91
+ } else if (line.startsWith("data:")) {
92
+ currentData = line.slice(5).trim();
93
+ } else if (line === "") {
94
+ if (currentData) {
95
+ this.handleEvent(currentEvent, currentData);
96
+ }
97
+ currentEvent = "";
98
+ currentData = "";
99
+ }
100
+ }
101
+ }
102
+ } catch (err) {
103
+ if (this.destroyed) return;
104
+ if (err instanceof Error && err.name === "AbortError") return;
105
+ this.onError(err instanceof Error ? err : new Error(String(err)));
106
+ } finally {
107
+ reader.releaseLock();
108
+ }
109
+ if (!this.destroyed) {
110
+ this.reconnect();
111
+ }
112
+ }
113
+ handleEvent(eventType, data) {
114
+ try {
115
+ const parsed = JSON.parse(data);
116
+ if (eventType && !parsed.type) {
117
+ parsed.type = eventType;
118
+ }
119
+ this.onEvent(parsed);
120
+ } catch {
121
+ }
122
+ }
123
+ reconnect() {
124
+ if (this.destroyed) return;
125
+ const delay = this.getBackoffDelay();
126
+ this.retryCount++;
127
+ this.retryTimeout = setTimeout(() => {
128
+ this.retryTimeout = null;
129
+ this.connect();
130
+ }, delay);
131
+ }
132
+ getBackoffDelay() {
133
+ const delay = this.retryDelay * Math.pow(2, this.retryCount);
134
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
135
+ return Math.min(delay + jitter, this.maxRetryDelay);
136
+ }
137
+ };
138
+
139
+ // src/client.ts
140
+ var FlaggyClient = class {
141
+ constructor(options) {
142
+ this.cache = /* @__PURE__ */ new Map();
143
+ this._ready = false;
144
+ this._error = null;
145
+ this.sseManager = null;
146
+ this.contextAbortController = null;
147
+ this.listeners = {
148
+ change: /* @__PURE__ */ new Set(),
149
+ ready: /* @__PURE__ */ new Set(),
150
+ error: /* @__PURE__ */ new Set()
151
+ };
152
+ this.serverUrl = options.serverUrl.replace(/\/$/, "");
153
+ this.apiKey = options.apiKey;
154
+ this.context = options.context ?? {};
155
+ this.enableStreaming = options.enableStreaming ?? true;
156
+ this.sseRetryDelay = options.sseRetryDelay ?? 1e3;
157
+ this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 3e4;
158
+ }
159
+ get ready() {
160
+ return this._ready;
161
+ }
162
+ get error() {
163
+ return this._error;
164
+ }
165
+ async initialize() {
166
+ try {
167
+ const response = await this.fetchApi(
168
+ "/api/v1/evaluate/batch",
169
+ { context: this.context }
170
+ );
171
+ this.applyBatchResult(response);
172
+ this._ready = true;
173
+ this.emit("ready");
174
+ } catch (err) {
175
+ this._error = err instanceof Error ? err : new Error(String(err));
176
+ this.emit("error", this._error);
177
+ }
178
+ if (this.enableStreaming) {
179
+ this.startSSE();
180
+ }
181
+ }
182
+ getFlag(key, defaultValue) {
183
+ if (!this._ready || !this.cache.has(key)) {
184
+ return defaultValue;
185
+ }
186
+ return this.cache.get(key);
187
+ }
188
+ async setContext(context) {
189
+ this.context = context;
190
+ this.contextAbortController?.abort();
191
+ const controller = new AbortController();
192
+ this.contextAbortController = controller;
193
+ try {
194
+ const response = await this.fetchApi(
195
+ "/api/v1/evaluate/batch",
196
+ { context },
197
+ controller.signal
198
+ );
199
+ if (controller.signal.aborted) return;
200
+ this.applyBatchResult(response);
201
+ } catch (err) {
202
+ if (err instanceof Error && err.name === "AbortError") return;
203
+ this._error = err instanceof Error ? err : new Error(String(err));
204
+ this.emit("error", this._error);
205
+ }
206
+ }
207
+ on(event, listener) {
208
+ this.listeners[event].add(listener);
209
+ return () => {
210
+ this.listeners[event].delete(listener);
211
+ };
212
+ }
213
+ destroy() {
214
+ this.sseManager?.destroy();
215
+ this.sseManager = null;
216
+ this.contextAbortController?.abort();
217
+ this.contextAbortController = null;
218
+ this.listeners.change.clear();
219
+ this.listeners.ready.clear();
220
+ this.listeners.error.clear();
221
+ }
222
+ startSSE() {
223
+ this.sseManager = new SSEManager({
224
+ url: `${this.serverUrl}/api/v1/stream`,
225
+ apiKey: this.apiKey,
226
+ onEvent: (event) => this.handleSSEEvent(event),
227
+ onError: (err) => this.emit("error", err),
228
+ retryDelay: this.sseRetryDelay,
229
+ maxRetryDelay: this.sseMaxRetryDelay
230
+ });
231
+ this.sseManager.connect();
232
+ }
233
+ async handleSSEEvent(event) {
234
+ if (event.type === "flag_deleted") {
235
+ if (this.cache.has(event.key)) {
236
+ this.cache.delete(event.key);
237
+ this.emit("change", event.key, void 0);
238
+ }
239
+ return;
240
+ }
241
+ try {
242
+ const response = await this.fetchApi(
243
+ "/api/v1/evaluate",
244
+ { flag_key: event.key, context: this.context }
245
+ );
246
+ const oldValue = this.cache.get(event.key);
247
+ this.cache.set(event.key, response.value);
248
+ if (oldValue !== response.value) {
249
+ this.emit("change", event.key, response.value);
250
+ }
251
+ } catch {
252
+ }
253
+ }
254
+ applyBatchResult(response) {
255
+ const newCache = /* @__PURE__ */ new Map();
256
+ for (const flag of response.results) {
257
+ newCache.set(flag.flag_key, flag.value);
258
+ }
259
+ for (const [key, newValue] of newCache) {
260
+ const oldValue = this.cache.get(key);
261
+ if (oldValue !== newValue) {
262
+ this.emit("change", key, newValue);
263
+ }
264
+ }
265
+ for (const key of this.cache.keys()) {
266
+ if (!newCache.has(key)) {
267
+ this.emit("change", key, void 0);
268
+ }
269
+ }
270
+ this.cache = newCache;
271
+ }
272
+ emit(event, ...args) {
273
+ if (event === "change") {
274
+ for (const listener of this.listeners.change) {
275
+ listener(args[0], args[1]);
276
+ }
277
+ } else if (event === "ready") {
278
+ for (const listener of this.listeners.ready) {
279
+ listener();
280
+ }
281
+ } else if (event === "error") {
282
+ for (const listener of this.listeners.error) {
283
+ listener(args[0]);
284
+ }
285
+ }
286
+ }
287
+ async fetchApi(path, body, signal) {
288
+ const response = await fetch(`${this.serverUrl}${path}`, {
289
+ method: "POST",
290
+ headers: {
291
+ "Content-Type": "application/json",
292
+ Authorization: `Bearer ${this.apiKey}`
293
+ },
294
+ body: JSON.stringify(body),
295
+ signal
296
+ });
297
+ if (!response.ok) {
298
+ throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);
299
+ }
300
+ return response.json();
301
+ }
302
+ };
303
+ // Annotate the CommonJS export names for ESM import in node:
304
+ 0 && (module.exports = {
305
+ FlaggyClient
306
+ });
307
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/sse.ts","../src/client.ts"],"sourcesContent":["export { FlaggyClient } from './client';\nexport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluatedFlag,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n","import type { FlagChangeEvent } from './types';\n\nexport interface SSEManagerOptions {\n url: string;\n apiKey: string;\n onEvent: (event: FlagChangeEvent) => void;\n onError: (error: Error) => void;\n retryDelay?: number;\n maxRetryDelay?: number;\n}\n\nexport class SSEManager {\n private abortController: AbortController | null = null;\n private retryCount = 0;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n\n private readonly url: string;\n private readonly apiKey: string;\n private readonly onEvent: (event: FlagChangeEvent) => void;\n private readonly onError: (error: Error) => void;\n private readonly retryDelay: number;\n private readonly maxRetryDelay: number;\n\n constructor(options: SSEManagerOptions) {\n this.url = options.url;\n this.apiKey = options.apiKey;\n this.onEvent = options.onEvent;\n this.onError = options.onError;\n this.retryDelay = options.retryDelay ?? 1000;\n this.maxRetryDelay = options.maxRetryDelay ?? 30_000;\n }\n\n connect(): void {\n if (this.destroyed) return;\n\n this.abortController = new AbortController();\n\n fetch(this.url, {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n Accept: 'text/event-stream',\n },\n signal: this.abortController.signal,\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error(`SSE connection failed: ${response.status}`);\n }\n if (!response.body) {\n throw new Error('SSE response has no body');\n }\n\n this.retryCount = 0;\n this.readStream(response.body);\n })\n .catch((err: unknown) => {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n this.reconnect();\n });\n }\n\n destroy(): void {\n this.destroyed = true;\n this.abortController?.abort();\n this.abortController = null;\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n }\n\n private async readStream(body: ReadableStream<Uint8Array>): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEvent = '';\n let currentData = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('event:')) {\n currentEvent = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n currentData = line.slice(5).trim();\n } else if (line === '') {\n // Empty line = end of event\n if (currentData) {\n this.handleEvent(currentEvent, currentData);\n }\n currentEvent = '';\n currentData = '';\n }\n }\n }\n } catch (err: unknown) {\n if (this.destroyed) return;\n if (err instanceof Error && err.name === 'AbortError') return;\n\n this.onError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n reader.releaseLock();\n }\n\n // Stream ended — reconnect if not destroyed\n if (!this.destroyed) {\n this.reconnect();\n }\n }\n\n private handleEvent(eventType: string, data: string): void {\n try {\n const parsed = JSON.parse(data) as FlagChangeEvent;\n // Use the event type from the SSE field if present, otherwise from data\n if (eventType && !parsed.type) {\n parsed.type = eventType as FlagChangeEvent['type'];\n }\n this.onEvent(parsed);\n } catch {\n // Malformed event data, skip\n }\n }\n\n private reconnect(): void {\n if (this.destroyed) return;\n\n const delay = this.getBackoffDelay();\n this.retryCount++;\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n this.connect();\n }, delay);\n }\n\n private getBackoffDelay(): number {\n const delay = this.retryDelay * Math.pow(2, this.retryCount);\n const jitter = delay * 0.25 * (Math.random() * 2 - 1);\n return Math.min(delay + jitter, this.maxRetryDelay);\n }\n}\n","import { SSEManager } from './sse';\nimport type {\n FlagValue,\n FlaggyContext,\n FlaggyClientOptions,\n EvaluateResponse,\n BatchEvaluateResponse,\n FlagChangeEvent,\n FlagChangeListener,\n ReadyListener,\n ErrorListener,\n} from './types';\n\ntype EventMap = {\n change: FlagChangeListener;\n ready: ReadyListener;\n error: ErrorListener;\n};\n\nexport class FlaggyClient {\n private readonly serverUrl: string;\n private readonly apiKey: string;\n private readonly enableStreaming: boolean;\n private readonly sseRetryDelay: number;\n private readonly sseMaxRetryDelay: number;\n\n private context: FlaggyContext;\n private cache = new Map<string, FlagValue>();\n private _ready = false;\n private _error: Error | null = null;\n private sseManager: SSEManager | null = null;\n private contextAbortController: AbortController | null = null;\n\n private listeners: {\n change: Set<FlagChangeListener>;\n ready: Set<ReadyListener>;\n error: Set<ErrorListener>;\n } = {\n change: new Set(),\n ready: new Set(),\n error: new Set(),\n };\n\n constructor(options: FlaggyClientOptions) {\n this.serverUrl = options.serverUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.context = options.context ?? {};\n this.enableStreaming = options.enableStreaming ?? true;\n this.sseRetryDelay = options.sseRetryDelay ?? 1000;\n this.sseMaxRetryDelay = options.sseMaxRetryDelay ?? 30_000;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get error(): Error | null {\n return this._error;\n }\n\n async initialize(): Promise<void> {\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context: this.context },\n );\n this.applyBatchResult(response);\n this._ready = true;\n this.emit('ready');\n } catch (err: unknown) {\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n\n if (this.enableStreaming) {\n this.startSSE();\n }\n }\n\n getFlag<T extends FlagValue>(key: string, defaultValue: T): T {\n if (!this._ready || !this.cache.has(key)) {\n return defaultValue;\n }\n return this.cache.get(key) as T;\n }\n\n async setContext(context: FlaggyContext): Promise<void> {\n this.context = context;\n this.contextAbortController?.abort();\n const controller = new AbortController();\n this.contextAbortController = controller;\n\n try {\n const response = await this.fetchApi<BatchEvaluateResponse>(\n '/api/v1/evaluate/batch',\n { context },\n controller.signal,\n );\n if (controller.signal.aborted) return;\n this.applyBatchResult(response);\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'AbortError') return;\n this._error = err instanceof Error ? err : new Error(String(err));\n this.emit('error', this._error);\n }\n }\n\n on<E extends keyof EventMap>(event: E, listener: EventMap[E]): () => void {\n (this.listeners[event] as Set<EventMap[E]>).add(listener);\n return () => {\n (this.listeners[event] as Set<EventMap[E]>).delete(listener);\n };\n }\n\n destroy(): void {\n this.sseManager?.destroy();\n this.sseManager = null;\n this.contextAbortController?.abort();\n this.contextAbortController = null;\n this.listeners.change.clear();\n this.listeners.ready.clear();\n this.listeners.error.clear();\n }\n\n private startSSE(): void {\n this.sseManager = new SSEManager({\n url: `${this.serverUrl}/api/v1/stream`,\n apiKey: this.apiKey,\n onEvent: (event) => this.handleSSEEvent(event),\n onError: (err) => this.emit('error', err),\n retryDelay: this.sseRetryDelay,\n maxRetryDelay: this.sseMaxRetryDelay,\n });\n this.sseManager.connect();\n }\n\n private async handleSSEEvent(event: FlagChangeEvent): Promise<void> {\n if (event.type === 'flag_deleted') {\n if (this.cache.has(event.key)) {\n this.cache.delete(event.key);\n this.emit('change', event.key, undefined as unknown as FlagValue);\n }\n return;\n }\n\n // flag_updated or flag_created — re-evaluate this flag\n try {\n const response = await this.fetchApi<EvaluateResponse>(\n '/api/v1/evaluate',\n { flag_key: event.key, context: this.context },\n );\n const oldValue = this.cache.get(event.key);\n this.cache.set(event.key, response.value);\n if (oldValue !== response.value) {\n this.emit('change', event.key, response.value);\n }\n } catch {\n // Failed to re-evaluate, keep previous cached value\n }\n }\n\n private applyBatchResult(response: BatchEvaluateResponse): void {\n const newCache = new Map<string, FlagValue>();\n for (const flag of response.results) {\n newCache.set(flag.flag_key, flag.value);\n }\n\n // Emit changes for any values that differ\n for (const [key, newValue] of newCache) {\n const oldValue = this.cache.get(key);\n if (oldValue !== newValue) {\n this.emit('change', key, newValue);\n }\n }\n\n // Emit changes for keys that were removed\n for (const key of this.cache.keys()) {\n if (!newCache.has(key)) {\n this.emit('change', key, undefined as unknown as FlagValue);\n }\n }\n\n this.cache = newCache;\n }\n\n private emit(event: 'change', key: string, value: FlagValue): void;\n private emit(event: 'ready'): void;\n private emit(event: 'error', error: Error): void;\n private emit(event: keyof EventMap, ...args: unknown[]): void {\n if (event === 'change') {\n for (const listener of this.listeners.change) {\n listener(args[0] as string, args[1] as FlagValue);\n }\n } else if (event === 'ready') {\n for (const listener of this.listeners.ready) {\n listener();\n }\n } else if (event === 'error') {\n for (const listener of this.listeners.error) {\n listener(args[0] as Error);\n }\n }\n }\n\n private async fetchApi<T>(\n path: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<T> {\n const response = await fetch(`${this.serverUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new Error(`Flaggy API error: ${response.status} ${response.statusText}`);\n }\n return response.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}