@rolloutly/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +63 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +63 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6,9 +6,16 @@ var database = require('firebase/database');
|
|
|
6
6
|
// src/client.ts
|
|
7
7
|
var DEFAULT_BASE_URL = "https://rolloutly.com";
|
|
8
8
|
var CACHE_KEY = "rolloutly_flags";
|
|
9
|
+
var toCamelCase = (str) => {
|
|
10
|
+
return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
|
|
11
|
+
};
|
|
12
|
+
var needsCamelCase = (key) => {
|
|
13
|
+
return key.includes("-") || key.includes("_");
|
|
14
|
+
};
|
|
9
15
|
var RolloutlyClient = class {
|
|
10
16
|
constructor(config) {
|
|
11
17
|
this.flags = {};
|
|
18
|
+
this.flagValuesCache = {};
|
|
12
19
|
this.status = "initializing";
|
|
13
20
|
this.error = null;
|
|
14
21
|
this.listeners = /* @__PURE__ */ new Set();
|
|
@@ -42,11 +49,19 @@ var RolloutlyClient = class {
|
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
51
|
* Get a single flag value
|
|
52
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
45
53
|
*/
|
|
46
54
|
getFlag(key) {
|
|
47
|
-
|
|
55
|
+
let flag = this.flags[key];
|
|
56
|
+
if (!flag) {
|
|
57
|
+
const originalKey = this.findOriginalKey(key);
|
|
58
|
+
if (originalKey) {
|
|
59
|
+
flag = this.flags[originalKey];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
48
62
|
if (flag) {
|
|
49
|
-
|
|
63
|
+
const defaultKey = this.findOriginalKey(key) || key;
|
|
64
|
+
return flag.enabled ? flag.value : this.config.defaultFlags[defaultKey];
|
|
50
65
|
}
|
|
51
66
|
return this.config.defaultFlags[key];
|
|
52
67
|
}
|
|
@@ -56,16 +71,32 @@ var RolloutlyClient = class {
|
|
|
56
71
|
getFlags() {
|
|
57
72
|
return { ...this.flags };
|
|
58
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Get all flag values as a stable object (for React hooks)
|
|
76
|
+
* Returns a cached object that only changes when flags change
|
|
77
|
+
*/
|
|
78
|
+
getFlagValues() {
|
|
79
|
+
return this.flagValuesCache;
|
|
80
|
+
}
|
|
59
81
|
/**
|
|
60
82
|
* Check if a boolean flag is enabled
|
|
83
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
61
84
|
*/
|
|
62
85
|
isEnabled(key) {
|
|
63
|
-
|
|
86
|
+
let flag = this.flags[key];
|
|
87
|
+
let lookupKey = key;
|
|
88
|
+
if (!flag) {
|
|
89
|
+
const originalKey = this.findOriginalKey(key);
|
|
90
|
+
if (originalKey) {
|
|
91
|
+
flag = this.flags[originalKey];
|
|
92
|
+
lookupKey = originalKey;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
64
95
|
if (!flag) {
|
|
65
|
-
return Boolean(this.config.defaultFlags[
|
|
96
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
66
97
|
}
|
|
67
98
|
if (!flag.enabled) {
|
|
68
|
-
return Boolean(this.config.defaultFlags[
|
|
99
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
69
100
|
}
|
|
70
101
|
return flag.type === "boolean" ? Boolean(flag.value) : true;
|
|
71
102
|
}
|
|
@@ -109,6 +140,21 @@ var RolloutlyClient = class {
|
|
|
109
140
|
this.log("Client closed");
|
|
110
141
|
}
|
|
111
142
|
// ==================== Private Methods ====================
|
|
143
|
+
/**
|
|
144
|
+
* Find the original flag key from a camelCase version
|
|
145
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
146
|
+
*/
|
|
147
|
+
findOriginalKey(camelKey) {
|
|
148
|
+
if (this.flags[camelKey]) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
for (const key of Object.keys(this.flags)) {
|
|
152
|
+
if (needsCamelCase(key) && toCamelCase(key) === camelKey) {
|
|
153
|
+
return key;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
112
158
|
parseToken(token) {
|
|
113
159
|
const parts = token.split("_");
|
|
114
160
|
if (parts.length < 4 || parts[0] !== "rly") {
|
|
@@ -212,6 +258,7 @@ var RolloutlyClient = class {
|
|
|
212
258
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
213
259
|
if (cached) {
|
|
214
260
|
this.flags = JSON.parse(cached);
|
|
261
|
+
this.updateFlagValuesCache();
|
|
215
262
|
this.log("Loaded cached flags");
|
|
216
263
|
}
|
|
217
264
|
} catch {
|
|
@@ -224,7 +271,18 @@ var RolloutlyClient = class {
|
|
|
224
271
|
} catch {
|
|
225
272
|
}
|
|
226
273
|
}
|
|
274
|
+
updateFlagValuesCache() {
|
|
275
|
+
this.flagValuesCache = Object.entries(this.flags).reduce((acc, [key, flag]) => {
|
|
276
|
+
acc[key] = flag.value;
|
|
277
|
+
if (needsCamelCase(key)) {
|
|
278
|
+
const camelKey = toCamelCase(key);
|
|
279
|
+
acc[camelKey] = flag.value;
|
|
280
|
+
}
|
|
281
|
+
return acc;
|
|
282
|
+
}, {});
|
|
283
|
+
}
|
|
227
284
|
notifyListeners() {
|
|
285
|
+
this.updateFlagValuesCache();
|
|
228
286
|
this.listeners.forEach((listener) => listener());
|
|
229
287
|
}
|
|
230
288
|
log(...args) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"names":["ref","off","initializeApp","getDatabase","onValue"],"mappings":"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","file":"index.cjs","sourcesContent":["import { initializeApp, type FirebaseApp } from 'firebase/app';\nimport {\n getDatabase,\n ref,\n onValue,\n off,\n type Database,\n type Unsubscribe,\n} from 'firebase/database';\n\nimport type {\n ClientStatus,\n Flag,\n FlagChangeListener,\n FlagMap,\n FlagValue,\n ParsedToken,\n RolloutlyConfig,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://rolloutly.com';\nconst CACHE_KEY = 'rolloutly_flags';\n\nexport class RolloutlyClient {\n private config: Required<\n Omit<RolloutlyConfig, 'defaultFlags'> & {\n defaultFlags: Record<string, FlagValue>;\n }\n >;\n private flags: FlagMap = {};\n private status: ClientStatus = 'initializing';\n private error: Error | null = null;\n private listeners: Set<FlagChangeListener> = new Set();\n private parsedToken: ParsedToken;\n private firebaseApp: FirebaseApp | null = null;\n private database: Database | null = null;\n private realtimeUnsubscribe: Unsubscribe | null = null;\n private initPromise: Promise<void>;\n private initResolve!: () => void;\n private initReject!: (error: Error) => void;\n\n constructor(config: RolloutlyConfig) {\n this.config = {\n token: config.token,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n realtimeEnabled: config.realtimeEnabled ?? true,\n defaultFlags: config.defaultFlags ?? {},\n debug: config.debug ?? false,\n };\n\n // Parse the token\n const parsed = this.parseToken(config.token);\n\n if (!parsed) {\n throw new Error('Invalid SDK token format');\n }\n\n this.parsedToken = parsed;\n\n // Create init promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve;\n this.initReject = reject;\n });\n\n // Load cached flags first\n this.loadCachedFlags();\n\n // Start initialization\n void this.initialize();\n }\n\n /**\n * Wait for the client to be initialized\n */\n async waitForInit(): Promise<void> {\n return this.initPromise;\n }\n\n /**\n * Get a single flag value\n */\n getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined {\n const flag = this.flags[key];\n\n if (flag) {\n return (flag.enabled ? flag.value : this.config.defaultFlags[key]) as T;\n }\n\n return this.config.defaultFlags[key] as T;\n }\n\n /**\n * Get all flags\n */\n getFlags(): FlagMap {\n return { ...this.flags };\n }\n\n /**\n * Check if a boolean flag is enabled\n */\n isEnabled(key: string): boolean {\n const flag = this.flags[key];\n\n if (!flag) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n if (!flag.enabled) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n return flag.type === 'boolean' ? Boolean(flag.value) : true;\n }\n\n /**\n * Get current client status\n */\n getStatus(): ClientStatus {\n return this.status;\n }\n\n /**\n * Get the last error if any\n */\n getError(): Error | null {\n return this.error;\n }\n\n /**\n * Subscribe to flag changes (for React useSyncExternalStore)\n */\n subscribe(listener: FlagChangeListener): () => void {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Cleanup and disconnect\n */\n close(): void {\n if (this.realtimeUnsubscribe) {\n this.realtimeUnsubscribe();\n this.realtimeUnsubscribe = null;\n }\n\n if (this.database) {\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n off(flagsRef);\n }\n\n this.listeners.clear();\n this.log('Client closed');\n }\n\n // ==================== Private Methods ====================\n\n private parseToken(token: string): ParsedToken | null {\n const parts = token.split('_');\n\n // Format: rly_{projectId}_{environmentKey}_{randomString}\n if (parts.length < 4 || parts[0] !== 'rly') {\n return null;\n }\n\n return {\n projectId: parts[1],\n environmentKey: parts[2],\n };\n }\n\n private async initialize(): Promise<void> {\n try {\n // Fetch initial flags from API\n await this.fetchFlags();\n\n // Set up real-time updates if enabled\n if (this.config.realtimeEnabled) {\n await this.setupRealtime();\n }\n\n this.status = 'ready';\n this.initResolve();\n this.log('Client initialized');\n } catch (err) {\n this.status = 'error';\n this.error = err instanceof Error ? err : new Error(String(err));\n this.initReject(this.error);\n this.log('Initialization failed:', this.error.message);\n }\n }\n\n private async fetchFlags(): Promise<void> {\n const url = `${this.config.baseUrl}/api/sdk/flags`;\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch flags: ${response.status}`);\n }\n\n const data = (await response.json()) as { flags: FlagMap };\n this.flags = data.flags;\n this.cacheFlags();\n this.notifyListeners();\n this.log('Flags fetched:', Object.keys(this.flags).length);\n }\n\n private async setupRealtime(): Promise<void> {\n // Initialize Firebase with minimal config for Realtime DB\n // The database URL is derived from the API response or configured\n const databaseURL = await this.getDatabaseUrl();\n\n if (!databaseURL) {\n this.log('Realtime DB URL not available, skipping real-time updates');\n\n return;\n }\n\n this.firebaseApp = initializeApp(\n {\n databaseURL,\n },\n `rolloutly-${Date.now()}`,\n );\n\n this.database = getDatabase(this.firebaseApp);\n\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n\n this.realtimeUnsubscribe = onValue(\n flagsRef,\n (snapshot) => {\n const data = snapshot.val() as Record<string, Flag> | null;\n\n if (data) {\n // Convert realtime format to our flag format\n this.flags = Object.entries(data).reduce<FlagMap>(\n (acc, [key, flag]) => {\n acc[key] = { ...flag, key };\n\n return acc;\n },\n {},\n );\n this.cacheFlags();\n this.notifyListeners();\n this.log('Realtime update received');\n }\n },\n (error) => {\n this.log('Realtime error:', error.message);\n },\n );\n }\n\n private async getDatabaseUrl(): Promise<string | null> {\n // Try to get the database URL from the API\n try {\n const url = `${this.config.baseUrl}/api/sdk/config`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (response.ok) {\n const data = (await response.json()) as { databaseUrl?: string };\n\n return data.databaseUrl ?? null;\n }\n } catch {\n // Ignore - we'll try without realtime\n }\n\n return null;\n }\n\n private loadCachedFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const cached = localStorage.getItem(CACHE_KEY);\n\n if (cached) {\n this.flags = JSON.parse(cached) as FlagMap;\n this.log('Loaded cached flags');\n }\n } catch {\n // Ignore cache errors\n }\n }\n\n private cacheFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));\n } catch {\n // Ignore cache errors\n }\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => listener());\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Rolloutly]', ...args);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":["ref","off","initializeApp","getDatabase","onValue"],"mappings":"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","file":"index.cjs","sourcesContent":["import { initializeApp, type FirebaseApp } from 'firebase/app';\nimport {\n getDatabase,\n off,\n onValue,\n ref,\n type Database,\n type Unsubscribe,\n} from 'firebase/database';\n\nimport type {\n ClientStatus,\n Flag,\n FlagChangeListener,\n FlagMap,\n FlagValue,\n ParsedToken,\n RolloutlyConfig,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://rolloutly.com';\nconst CACHE_KEY = 'rolloutly_flags';\n\n/**\n * Convert a string to camelCase\n * 'instagram-integration' -> 'instagramIntegration'\n * 'my_feature_flag' -> 'myFeatureFlag'\n */\nconst toCamelCase = (str: string): string => {\n return str.replace(/[-_](.)/g, (_, char: string) => char.toUpperCase());\n};\n\n/**\n * Check if a key needs camelCase conversion (has hyphens or underscores)\n */\nconst needsCamelCase = (key: string): boolean => {\n return key.includes('-') || key.includes('_');\n};\n\nexport class RolloutlyClient {\n private config: Required<\n Omit<RolloutlyConfig, 'defaultFlags'> & {\n defaultFlags: Record<string, FlagValue>;\n }\n >;\n private flags: FlagMap = {};\n private flagValuesCache: Record<string, FlagValue | undefined> = {};\n private status: ClientStatus = 'initializing';\n private error: Error | null = null;\n private listeners: Set<FlagChangeListener> = new Set();\n private parsedToken: ParsedToken;\n private firebaseApp: FirebaseApp | null = null;\n private database: Database | null = null;\n private realtimeUnsubscribe: Unsubscribe | null = null;\n private initPromise: Promise<void>;\n private initResolve!: () => void;\n private initReject!: (error: Error) => void;\n\n constructor(config: RolloutlyConfig) {\n this.config = {\n token: config.token,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n realtimeEnabled: config.realtimeEnabled ?? true,\n defaultFlags: config.defaultFlags ?? {},\n debug: config.debug ?? false,\n };\n\n // Parse the token\n const parsed = this.parseToken(config.token);\n\n if (!parsed) {\n throw new Error('Invalid SDK token format');\n }\n\n this.parsedToken = parsed;\n\n // Create init promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve;\n this.initReject = reject;\n });\n\n // Load cached flags first\n this.loadCachedFlags();\n\n // Start initialization\n void this.initialize();\n }\n\n /**\n * Wait for the client to be initialized\n */\n async waitForInit(): Promise<void> {\n return this.initPromise;\n }\n\n /**\n * Get a single flag value\n * Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')\n */\n getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined {\n // Try direct lookup first\n let flag = this.flags[key];\n\n // If not found and key is camelCase, try to find original key\n if (!flag) {\n const originalKey = this.findOriginalKey(key);\n\n if (originalKey) {\n flag = this.flags[originalKey];\n }\n }\n\n if (flag) {\n const defaultKey = this.findOriginalKey(key) || key;\n\n return (\n flag.enabled ? flag.value : this.config.defaultFlags[defaultKey]\n ) as T;\n }\n\n return this.config.defaultFlags[key] as T;\n }\n\n /**\n * Get all flags\n */\n getFlags(): FlagMap {\n return { ...this.flags };\n }\n\n /**\n * Get all flag values as a stable object (for React hooks)\n * Returns a cached object that only changes when flags change\n */\n getFlagValues(): Record<string, FlagValue | undefined> {\n return this.flagValuesCache;\n }\n\n /**\n * Check if a boolean flag is enabled\n * Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')\n */\n isEnabled(key: string): boolean {\n // Try direct lookup first\n let flag = this.flags[key];\n let lookupKey = key;\n\n // If not found and key is camelCase, try to find original key\n if (!flag) {\n const originalKey = this.findOriginalKey(key);\n\n if (originalKey) {\n flag = this.flags[originalKey];\n lookupKey = originalKey;\n }\n }\n\n if (!flag) {\n return Boolean(this.config.defaultFlags[lookupKey]);\n }\n\n if (!flag.enabled) {\n return Boolean(this.config.defaultFlags[lookupKey]);\n }\n\n return flag.type === 'boolean' ? Boolean(flag.value) : true;\n }\n\n /**\n * Get current client status\n */\n getStatus(): ClientStatus {\n return this.status;\n }\n\n /**\n * Get the last error if any\n */\n getError(): Error | null {\n return this.error;\n }\n\n /**\n * Subscribe to flag changes (for React useSyncExternalStore)\n */\n subscribe(listener: FlagChangeListener): () => void {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Cleanup and disconnect\n */\n close(): void {\n if (this.realtimeUnsubscribe) {\n this.realtimeUnsubscribe();\n this.realtimeUnsubscribe = null;\n }\n\n if (this.database) {\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n off(flagsRef);\n }\n\n this.listeners.clear();\n this.log('Client closed');\n }\n\n // ==================== Private Methods ====================\n\n /**\n * Find the original flag key from a camelCase version\n * 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)\n */\n private findOriginalKey(camelKey: string): string | null {\n // If the key exists directly, return null (no conversion needed)\n if (this.flags[camelKey]) {\n return null;\n }\n\n // Search for a flag whose camelCase version matches\n for (const key of Object.keys(this.flags)) {\n if (needsCamelCase(key) && toCamelCase(key) === camelKey) {\n return key;\n }\n }\n\n return null;\n }\n\n private parseToken(token: string): ParsedToken | null {\n const parts = token.split('_');\n\n // Format: rly_{projectId}_{environmentKey}_{randomString}\n if (parts.length < 4 || parts[0] !== 'rly') {\n return null;\n }\n\n return {\n projectId: parts[1],\n environmentKey: parts[2],\n };\n }\n\n private async initialize(): Promise<void> {\n try {\n // Fetch initial flags from API\n await this.fetchFlags();\n\n // Set up real-time updates if enabled\n if (this.config.realtimeEnabled) {\n await this.setupRealtime();\n }\n\n this.status = 'ready';\n this.initResolve();\n this.log('Client initialized');\n } catch (err) {\n this.status = 'error';\n this.error = err instanceof Error ? err : new Error(String(err));\n this.initReject(this.error);\n this.log('Initialization failed:', this.error.message);\n }\n }\n\n private async fetchFlags(): Promise<void> {\n const url = `${this.config.baseUrl}/api/sdk/flags`;\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch flags: ${response.status}`);\n }\n\n const data = (await response.json()) as { flags: FlagMap };\n this.flags = data.flags;\n this.cacheFlags();\n this.notifyListeners();\n this.log('Flags fetched:', Object.keys(this.flags).length);\n }\n\n private async setupRealtime(): Promise<void> {\n // Initialize Firebase with minimal config for Realtime DB\n // The database URL is derived from the API response or configured\n const databaseURL = await this.getDatabaseUrl();\n\n if (!databaseURL) {\n this.log('Realtime DB URL not available, skipping real-time updates');\n\n return;\n }\n\n this.firebaseApp = initializeApp(\n {\n databaseURL,\n },\n `rolloutly-${Date.now()}`,\n );\n\n this.database = getDatabase(this.firebaseApp);\n\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n\n this.realtimeUnsubscribe = onValue(\n flagsRef,\n (snapshot) => {\n const data = snapshot.val() as Record<string, Flag> | null;\n\n if (data) {\n // Convert realtime format to our flag format\n this.flags = Object.entries(data).reduce<FlagMap>(\n (acc, [key, flag]) => {\n acc[key] = { ...flag, key };\n\n return acc;\n },\n {},\n );\n this.cacheFlags();\n this.notifyListeners();\n this.log('Realtime update received');\n }\n },\n (error) => {\n this.log('Realtime error:', error.message);\n },\n );\n }\n\n private async getDatabaseUrl(): Promise<string | null> {\n // Try to get the database URL from the API\n try {\n const url = `${this.config.baseUrl}/api/sdk/config`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (response.ok) {\n const data = (await response.json()) as { databaseUrl?: string };\n\n return data.databaseUrl ?? null;\n }\n } catch {\n // Ignore - we'll try without realtime\n }\n\n return null;\n }\n\n private loadCachedFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const cached = localStorage.getItem(CACHE_KEY);\n\n if (cached) {\n this.flags = JSON.parse(cached) as FlagMap;\n this.updateFlagValuesCache();\n this.log('Loaded cached flags');\n }\n } catch {\n // Ignore cache errors\n }\n }\n\n private cacheFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));\n } catch {\n // Ignore cache errors\n }\n }\n\n private updateFlagValuesCache(): void {\n // Create a new cache object only when flags change\n // Include both original keys and camelCase versions for easy destructuring\n this.flagValuesCache = Object.entries(this.flags).reduce<\n Record<string, FlagValue | undefined>\n >((acc, [key, flag]) => {\n // Always include original key\n acc[key] = flag.value;\n\n // If key has hyphens or underscores, also add camelCase version\n if (needsCamelCase(key)) {\n const camelKey = toCamelCase(key);\n acc[camelKey] = flag.value;\n }\n\n return acc;\n }, {});\n }\n\n private notifyListeners(): void {\n this.updateFlagValuesCache();\n this.listeners.forEach((listener) => listener());\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Rolloutly]', ...args);\n }\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -42,6 +42,7 @@ type FlagChangeListener = () => void;
|
|
|
42
42
|
declare class RolloutlyClient {
|
|
43
43
|
private config;
|
|
44
44
|
private flags;
|
|
45
|
+
private flagValuesCache;
|
|
45
46
|
private status;
|
|
46
47
|
private error;
|
|
47
48
|
private listeners;
|
|
@@ -59,14 +60,21 @@ declare class RolloutlyClient {
|
|
|
59
60
|
waitForInit(): Promise<void>;
|
|
60
61
|
/**
|
|
61
62
|
* Get a single flag value
|
|
63
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
62
64
|
*/
|
|
63
65
|
getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined;
|
|
64
66
|
/**
|
|
65
67
|
* Get all flags
|
|
66
68
|
*/
|
|
67
69
|
getFlags(): FlagMap;
|
|
70
|
+
/**
|
|
71
|
+
* Get all flag values as a stable object (for React hooks)
|
|
72
|
+
* Returns a cached object that only changes when flags change
|
|
73
|
+
*/
|
|
74
|
+
getFlagValues(): Record<string, FlagValue | undefined>;
|
|
68
75
|
/**
|
|
69
76
|
* Check if a boolean flag is enabled
|
|
77
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
70
78
|
*/
|
|
71
79
|
isEnabled(key: string): boolean;
|
|
72
80
|
/**
|
|
@@ -85,6 +93,11 @@ declare class RolloutlyClient {
|
|
|
85
93
|
* Cleanup and disconnect
|
|
86
94
|
*/
|
|
87
95
|
close(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Find the original flag key from a camelCase version
|
|
98
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
99
|
+
*/
|
|
100
|
+
private findOriginalKey;
|
|
88
101
|
private parseToken;
|
|
89
102
|
private initialize;
|
|
90
103
|
private fetchFlags;
|
|
@@ -92,6 +105,7 @@ declare class RolloutlyClient {
|
|
|
92
105
|
private getDatabaseUrl;
|
|
93
106
|
private loadCachedFlags;
|
|
94
107
|
private cacheFlags;
|
|
108
|
+
private updateFlagValuesCache;
|
|
95
109
|
private notifyListeners;
|
|
96
110
|
private log;
|
|
97
111
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ type FlagChangeListener = () => void;
|
|
|
42
42
|
declare class RolloutlyClient {
|
|
43
43
|
private config;
|
|
44
44
|
private flags;
|
|
45
|
+
private flagValuesCache;
|
|
45
46
|
private status;
|
|
46
47
|
private error;
|
|
47
48
|
private listeners;
|
|
@@ -59,14 +60,21 @@ declare class RolloutlyClient {
|
|
|
59
60
|
waitForInit(): Promise<void>;
|
|
60
61
|
/**
|
|
61
62
|
* Get a single flag value
|
|
63
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
62
64
|
*/
|
|
63
65
|
getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined;
|
|
64
66
|
/**
|
|
65
67
|
* Get all flags
|
|
66
68
|
*/
|
|
67
69
|
getFlags(): FlagMap;
|
|
70
|
+
/**
|
|
71
|
+
* Get all flag values as a stable object (for React hooks)
|
|
72
|
+
* Returns a cached object that only changes when flags change
|
|
73
|
+
*/
|
|
74
|
+
getFlagValues(): Record<string, FlagValue | undefined>;
|
|
68
75
|
/**
|
|
69
76
|
* Check if a boolean flag is enabled
|
|
77
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
70
78
|
*/
|
|
71
79
|
isEnabled(key: string): boolean;
|
|
72
80
|
/**
|
|
@@ -85,6 +93,11 @@ declare class RolloutlyClient {
|
|
|
85
93
|
* Cleanup and disconnect
|
|
86
94
|
*/
|
|
87
95
|
close(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Find the original flag key from a camelCase version
|
|
98
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
99
|
+
*/
|
|
100
|
+
private findOriginalKey;
|
|
88
101
|
private parseToken;
|
|
89
102
|
private initialize;
|
|
90
103
|
private fetchFlags;
|
|
@@ -92,6 +105,7 @@ declare class RolloutlyClient {
|
|
|
92
105
|
private getDatabaseUrl;
|
|
93
106
|
private loadCachedFlags;
|
|
94
107
|
private cacheFlags;
|
|
108
|
+
private updateFlagValuesCache;
|
|
95
109
|
private notifyListeners;
|
|
96
110
|
private log;
|
|
97
111
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,16 @@ import { ref, off, getDatabase, onValue } from 'firebase/database';
|
|
|
4
4
|
// src/client.ts
|
|
5
5
|
var DEFAULT_BASE_URL = "https://rolloutly.com";
|
|
6
6
|
var CACHE_KEY = "rolloutly_flags";
|
|
7
|
+
var toCamelCase = (str) => {
|
|
8
|
+
return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
|
|
9
|
+
};
|
|
10
|
+
var needsCamelCase = (key) => {
|
|
11
|
+
return key.includes("-") || key.includes("_");
|
|
12
|
+
};
|
|
7
13
|
var RolloutlyClient = class {
|
|
8
14
|
constructor(config) {
|
|
9
15
|
this.flags = {};
|
|
16
|
+
this.flagValuesCache = {};
|
|
10
17
|
this.status = "initializing";
|
|
11
18
|
this.error = null;
|
|
12
19
|
this.listeners = /* @__PURE__ */ new Set();
|
|
@@ -40,11 +47,19 @@ var RolloutlyClient = class {
|
|
|
40
47
|
}
|
|
41
48
|
/**
|
|
42
49
|
* Get a single flag value
|
|
50
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
43
51
|
*/
|
|
44
52
|
getFlag(key) {
|
|
45
|
-
|
|
53
|
+
let flag = this.flags[key];
|
|
54
|
+
if (!flag) {
|
|
55
|
+
const originalKey = this.findOriginalKey(key);
|
|
56
|
+
if (originalKey) {
|
|
57
|
+
flag = this.flags[originalKey];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
46
60
|
if (flag) {
|
|
47
|
-
|
|
61
|
+
const defaultKey = this.findOriginalKey(key) || key;
|
|
62
|
+
return flag.enabled ? flag.value : this.config.defaultFlags[defaultKey];
|
|
48
63
|
}
|
|
49
64
|
return this.config.defaultFlags[key];
|
|
50
65
|
}
|
|
@@ -54,16 +69,32 @@ var RolloutlyClient = class {
|
|
|
54
69
|
getFlags() {
|
|
55
70
|
return { ...this.flags };
|
|
56
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Get all flag values as a stable object (for React hooks)
|
|
74
|
+
* Returns a cached object that only changes when flags change
|
|
75
|
+
*/
|
|
76
|
+
getFlagValues() {
|
|
77
|
+
return this.flagValuesCache;
|
|
78
|
+
}
|
|
57
79
|
/**
|
|
58
80
|
* Check if a boolean flag is enabled
|
|
81
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
59
82
|
*/
|
|
60
83
|
isEnabled(key) {
|
|
61
|
-
|
|
84
|
+
let flag = this.flags[key];
|
|
85
|
+
let lookupKey = key;
|
|
86
|
+
if (!flag) {
|
|
87
|
+
const originalKey = this.findOriginalKey(key);
|
|
88
|
+
if (originalKey) {
|
|
89
|
+
flag = this.flags[originalKey];
|
|
90
|
+
lookupKey = originalKey;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
62
93
|
if (!flag) {
|
|
63
|
-
return Boolean(this.config.defaultFlags[
|
|
94
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
64
95
|
}
|
|
65
96
|
if (!flag.enabled) {
|
|
66
|
-
return Boolean(this.config.defaultFlags[
|
|
97
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
67
98
|
}
|
|
68
99
|
return flag.type === "boolean" ? Boolean(flag.value) : true;
|
|
69
100
|
}
|
|
@@ -107,6 +138,21 @@ var RolloutlyClient = class {
|
|
|
107
138
|
this.log("Client closed");
|
|
108
139
|
}
|
|
109
140
|
// ==================== Private Methods ====================
|
|
141
|
+
/**
|
|
142
|
+
* Find the original flag key from a camelCase version
|
|
143
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
144
|
+
*/
|
|
145
|
+
findOriginalKey(camelKey) {
|
|
146
|
+
if (this.flags[camelKey]) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
for (const key of Object.keys(this.flags)) {
|
|
150
|
+
if (needsCamelCase(key) && toCamelCase(key) === camelKey) {
|
|
151
|
+
return key;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
110
156
|
parseToken(token) {
|
|
111
157
|
const parts = token.split("_");
|
|
112
158
|
if (parts.length < 4 || parts[0] !== "rly") {
|
|
@@ -210,6 +256,7 @@ var RolloutlyClient = class {
|
|
|
210
256
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
211
257
|
if (cached) {
|
|
212
258
|
this.flags = JSON.parse(cached);
|
|
259
|
+
this.updateFlagValuesCache();
|
|
213
260
|
this.log("Loaded cached flags");
|
|
214
261
|
}
|
|
215
262
|
} catch {
|
|
@@ -222,7 +269,18 @@ var RolloutlyClient = class {
|
|
|
222
269
|
} catch {
|
|
223
270
|
}
|
|
224
271
|
}
|
|
272
|
+
updateFlagValuesCache() {
|
|
273
|
+
this.flagValuesCache = Object.entries(this.flags).reduce((acc, [key, flag]) => {
|
|
274
|
+
acc[key] = flag.value;
|
|
275
|
+
if (needsCamelCase(key)) {
|
|
276
|
+
const camelKey = toCamelCase(key);
|
|
277
|
+
acc[camelKey] = flag.value;
|
|
278
|
+
}
|
|
279
|
+
return acc;
|
|
280
|
+
}, {});
|
|
281
|
+
}
|
|
225
282
|
notifyListeners() {
|
|
283
|
+
this.updateFlagValuesCache();
|
|
226
284
|
this.listeners.forEach((listener) => listener());
|
|
227
285
|
}
|
|
228
286
|
log(...args) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":";;;;AAoBA,IAAM,gBAAA,GAAmB,uBAAA;AACzB,IAAM,SAAA,GAAY,iBAAA;AAEX,IAAM,kBAAN,MAAsB;AAAA,EAkB3B,YAAY,MAAA,EAAyB;AAZrC,IAAA,IAAA,CAAQ,QAAiB,EAAC;AAC1B,IAAA,IAAA,CAAQ,MAAA,GAAuB,cAAA;AAC/B,IAAA,IAAA,CAAQ,KAAA,GAAsB,IAAA;AAC9B,IAAA,IAAA,CAAQ,SAAA,uBAAyC,GAAA,EAAI;AAErD,IAAA,IAAA,CAAQ,WAAA,GAAkC,IAAA;AAC1C,IAAA,IAAA,CAAQ,QAAA,GAA4B,IAAA;AACpC,IAAA,IAAA,CAAQ,mBAAA,GAA0C,IAAA;AAMhD,IAAA,IAAA,CAAK,MAAA,GAAS;AAAA,MACZ,OAAO,MAAA,CAAO,KAAA;AAAA,MACd,OAAA,EAAS,OAAO,OAAA,IAAW,gBAAA;AAAA,MAC3B,eAAA,EAAiB,OAAO,eAAA,IAAmB,IAAA;AAAA,MAC3C,YAAA,EAAc,MAAA,CAAO,YAAA,IAAgB,EAAC;AAAA,MACtC,KAAA,EAAO,OAAO,KAAA,IAAS;AAAA,KACzB;AAGA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,MAAA,CAAO,KAAK,CAAA;AAE3C,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,MAAM,IAAI,MAAM,0BAA0B,CAAA;AAAA,IAC5C;AAEA,IAAA,IAAA,CAAK,WAAA,GAAc,MAAA;AAGnB,IAAA,IAAA,CAAK,WAAA,GAAc,IAAI,OAAA,CAAQ,CAAC,SAAS,MAAA,KAAW;AAClD,MAAA,IAAA,CAAK,WAAA,GAAc,OAAA;AACnB,MAAA,IAAA,CAAK,UAAA,GAAa,MAAA;AAAA,IACpB,CAAC,CAAA;AAGD,IAAA,IAAA,CAAK,eAAA,EAAgB;AAGrB,IAAA,KAAK,KAAK,UAAA,EAAW;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAA,GAA6B;AACjC,IAAA,OAAO,IAAA,CAAK,WAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,QAAyC,GAAA,EAA4B;AACnE,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAE3B,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,OAAQ,KAAK,OAAA,GAAU,IAAA,CAAK,QAAQ,IAAA,CAAK,MAAA,CAAO,aAAa,GAAG,CAAA;AAAA,IAClE;AAEA,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,GAAG,CAAA;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA,EAKA,QAAA,GAAoB;AAClB,IAAA,OAAO,EAAE,GAAG,IAAA,CAAK,KAAA,EAAM;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,GAAA,EAAsB;AAC9B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAE3B,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,GAAG,CAAC,CAAA;AAAA,IAC9C;AAEA,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACjB,MAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,GAAG,CAAC,CAAA;AAAA,IAC9C;AAEA,IAAA,OAAO,KAAK,IAAA,KAAS,SAAA,GAAY,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,SAAA,GAA0B;AACxB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,QAAA,GAAyB;AACvB,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,QAAA,EAA0C;AAClD,IAAA,IAAA,CAAK,SAAA,CAAU,IAAI,QAAQ,CAAA;AAE3B,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,CAAK,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,IAChC,CAAA;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAA,GAAc;AACZ,IAAA,IAAI,KAAK,mBAAA,EAAqB;AAC5B,MAAA,IAAA,CAAK,mBAAA,EAAoB;AACzB,MAAA,IAAA,CAAK,mBAAA,GAAsB,IAAA;AAAA,IAC7B;AAEA,IAAA,IAAI,KAAK,QAAA,EAAU;AACjB,MAAA,MAAM,QAAA,GAAW,GAAA;AAAA,QACf,IAAA,CAAK,QAAA;AAAA,QACL,SAAS,IAAA,CAAK,WAAA,CAAY,SAAS,CAAA,CAAA,EAAI,IAAA,CAAK,YAAY,cAAc,CAAA;AAAA,OACxE;AACA,MAAA,GAAA,CAAI,QAAQ,CAAA;AAAA,IACd;AAEA,IAAA,IAAA,CAAK,UAAU,KAAA,EAAM;AACrB,IAAA,IAAA,CAAK,IAAI,eAAe,CAAA;AAAA,EAC1B;AAAA;AAAA,EAIQ,WAAW,KAAA,EAAmC;AACpD,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA;AAG7B,IAAA,IAAI,MAAM,MAAA,GAAS,CAAA,IAAK,KAAA,CAAM,CAAC,MAAM,KAAA,EAAO;AAC1C,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO;AAAA,MACL,SAAA,EAAW,MAAM,CAAC,CAAA;AAAA,MAClB,cAAA,EAAgB,MAAM,CAAC;AAAA,KACzB;AAAA,EACF;AAAA,EAEA,MAAc,UAAA,GAA4B;AACxC,IAAA,IAAI;AAEF,MAAA,MAAM,KAAK,UAAA,EAAW;AAGtB,MAAA,IAAI,IAAA,CAAK,OAAO,eAAA,EAAiB;AAC/B,QAAA,MAAM,KAAK,aAAA,EAAc;AAAA,MAC3B;AAEA,MAAA,IAAA,CAAK,MAAA,GAAS,OAAA;AACd,MAAA,IAAA,CAAK,WAAA,EAAY;AACjB,MAAA,IAAA,CAAK,IAAI,oBAAoB,CAAA;AAAA,IAC/B,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,MAAA,GAAS,OAAA;AACd,MAAA,IAAA,CAAK,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAC/D,MAAA,IAAA,CAAK,UAAA,CAAW,KAAK,KAAK,CAAA;AAC1B,MAAA,IAAA,CAAK,GAAA,CAAI,wBAAA,EAA0B,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAAA,IACvD;AAAA,EACF;AAAA,EAEA,MAAc,UAAA,GAA4B;AACxC,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA,cAAA,CAAA;AAElC,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,MAChC,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA;AAAA;AAC5C,KACD,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAAA,IAC7D;AAEA,IAAA,MAAM,IAAA,GAAQ,MAAM,QAAA,CAAS,IAAA,EAAK;AAClC,IAAA,IAAA,CAAK,QAAQ,IAAA,CAAK,KAAA;AAClB,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAA,CAAK,eAAA,EAAgB;AACrB,IAAA,IAAA,CAAK,IAAI,gBAAA,EAAkB,MAAA,CAAO,KAAK,IAAA,CAAK,KAAK,EAAE,MAAM,CAAA;AAAA,EAC3D;AAAA,EAEA,MAAc,aAAA,GAA+B;AAG3C,IAAA,MAAM,WAAA,GAAc,MAAM,IAAA,CAAK,cAAA,EAAe;AAE9C,IAAA,IAAI,CAAC,WAAA,EAAa;AAChB,MAAA,IAAA,CAAK,IAAI,2DAA2D,CAAA;AAEpE,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,WAAA,GAAc,aAAA;AAAA,MACjB;AAAA,QACE;AAAA,OACF;AAAA,MACA,CAAA,UAAA,EAAa,IAAA,CAAK,GAAA,EAAK,CAAA;AAAA,KACzB;AAEA,IAAA,IAAA,CAAK,QAAA,GAAW,WAAA,CAAY,IAAA,CAAK,WAAW,CAAA;AAE5C,IAAA,MAAM,QAAA,GAAW,GAAA;AAAA,MACf,IAAA,CAAK,QAAA;AAAA,MACL,SAAS,IAAA,CAAK,WAAA,CAAY,SAAS,CAAA,CAAA,EAAI,IAAA,CAAK,YAAY,cAAc,CAAA;AAAA,KACxE;AAEA,IAAA,IAAA,CAAK,mBAAA,GAAsB,OAAA;AAAA,MACzB,QAAA;AAAA,MACA,CAAC,QAAA,KAAa;AACZ,QAAA,MAAM,IAAA,GAAO,SAAS,GAAA,EAAI;AAE1B,QAAA,IAAI,IAAA,EAAM;AAER,UAAA,IAAA,CAAK,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,CAAE,MAAA;AAAA,YAChC,CAAC,GAAA,EAAK,CAAC,GAAA,EAAK,IAAI,CAAA,KAAM;AACpB,cAAA,GAAA,CAAI,GAAG,CAAA,GAAI,EAAE,GAAG,MAAM,GAAA,EAAI;AAE1B,cAAA,OAAO,GAAA;AAAA,YACT,CAAA;AAAA,YACA;AAAC,WACH;AACA,UAAA,IAAA,CAAK,UAAA,EAAW;AAChB,UAAA,IAAA,CAAK,eAAA,EAAgB;AACrB,UAAA,IAAA,CAAK,IAAI,0BAA0B,CAAA;AAAA,QACrC;AAAA,MACF,CAAA;AAAA,MACA,CAAC,KAAA,KAAU;AACT,QAAA,IAAA,CAAK,GAAA,CAAI,iBAAA,EAAmB,KAAA,CAAM,OAAO,CAAA;AAAA,MAC3C;AAAA,KACF;AAAA,EACF;AAAA,EAEA,MAAc,cAAA,GAAyC;AAErD,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA,eAAA,CAAA;AAClC,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,OAAA,EAAS;AAAA,UACP,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA;AAAA;AAC5C,OACD,CAAA;AAED,MAAA,IAAI,SAAS,EAAA,EAAI;AACf,QAAA,MAAM,IAAA,GAAQ,MAAM,QAAA,CAAS,IAAA,EAAK;AAElC,QAAA,OAAO,KAAK,WAAA,IAAe,IAAA;AAAA,MAC7B;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEQ,eAAA,GAAwB;AAC9B,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAEnC,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,OAAA,CAAQ,SAAS,CAAA;AAE7C,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA;AAC9B,QAAA,IAAA,CAAK,IAAI,qBAAqB,CAAA;AAAA,MAChC;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,UAAA,GAAmB;AACzB,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAEnC,IAAA,IAAI;AACF,MAAA,YAAA,CAAa,QAAQ,SAAA,EAAW,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,IAC5D,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,eAAA,GAAwB;AAC9B,IAAA,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,CAAC,QAAA,KAAa,UAAU,CAAA;AAAA,EACjD;AAAA,EAEQ,OAAO,IAAA,EAAuB;AACpC,IAAA,IAAI,IAAA,CAAK,OAAO,KAAA,EAAO;AACrB,MAAA,OAAA,CAAQ,GAAA,CAAI,aAAA,EAAe,GAAG,IAAI,CAAA;AAAA,IACpC;AAAA,EACF;AACF","file":"index.js","sourcesContent":["import { initializeApp, type FirebaseApp } from 'firebase/app';\nimport {\n getDatabase,\n ref,\n onValue,\n off,\n type Database,\n type Unsubscribe,\n} from 'firebase/database';\n\nimport type {\n ClientStatus,\n Flag,\n FlagChangeListener,\n FlagMap,\n FlagValue,\n ParsedToken,\n RolloutlyConfig,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://rolloutly.com';\nconst CACHE_KEY = 'rolloutly_flags';\n\nexport class RolloutlyClient {\n private config: Required<\n Omit<RolloutlyConfig, 'defaultFlags'> & {\n defaultFlags: Record<string, FlagValue>;\n }\n >;\n private flags: FlagMap = {};\n private status: ClientStatus = 'initializing';\n private error: Error | null = null;\n private listeners: Set<FlagChangeListener> = new Set();\n private parsedToken: ParsedToken;\n private firebaseApp: FirebaseApp | null = null;\n private database: Database | null = null;\n private realtimeUnsubscribe: Unsubscribe | null = null;\n private initPromise: Promise<void>;\n private initResolve!: () => void;\n private initReject!: (error: Error) => void;\n\n constructor(config: RolloutlyConfig) {\n this.config = {\n token: config.token,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n realtimeEnabled: config.realtimeEnabled ?? true,\n defaultFlags: config.defaultFlags ?? {},\n debug: config.debug ?? false,\n };\n\n // Parse the token\n const parsed = this.parseToken(config.token);\n\n if (!parsed) {\n throw new Error('Invalid SDK token format');\n }\n\n this.parsedToken = parsed;\n\n // Create init promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve;\n this.initReject = reject;\n });\n\n // Load cached flags first\n this.loadCachedFlags();\n\n // Start initialization\n void this.initialize();\n }\n\n /**\n * Wait for the client to be initialized\n */\n async waitForInit(): Promise<void> {\n return this.initPromise;\n }\n\n /**\n * Get a single flag value\n */\n getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined {\n const flag = this.flags[key];\n\n if (flag) {\n return (flag.enabled ? flag.value : this.config.defaultFlags[key]) as T;\n }\n\n return this.config.defaultFlags[key] as T;\n }\n\n /**\n * Get all flags\n */\n getFlags(): FlagMap {\n return { ...this.flags };\n }\n\n /**\n * Check if a boolean flag is enabled\n */\n isEnabled(key: string): boolean {\n const flag = this.flags[key];\n\n if (!flag) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n if (!flag.enabled) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n return flag.type === 'boolean' ? Boolean(flag.value) : true;\n }\n\n /**\n * Get current client status\n */\n getStatus(): ClientStatus {\n return this.status;\n }\n\n /**\n * Get the last error if any\n */\n getError(): Error | null {\n return this.error;\n }\n\n /**\n * Subscribe to flag changes (for React useSyncExternalStore)\n */\n subscribe(listener: FlagChangeListener): () => void {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Cleanup and disconnect\n */\n close(): void {\n if (this.realtimeUnsubscribe) {\n this.realtimeUnsubscribe();\n this.realtimeUnsubscribe = null;\n }\n\n if (this.database) {\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n off(flagsRef);\n }\n\n this.listeners.clear();\n this.log('Client closed');\n }\n\n // ==================== Private Methods ====================\n\n private parseToken(token: string): ParsedToken | null {\n const parts = token.split('_');\n\n // Format: rly_{projectId}_{environmentKey}_{randomString}\n if (parts.length < 4 || parts[0] !== 'rly') {\n return null;\n }\n\n return {\n projectId: parts[1],\n environmentKey: parts[2],\n };\n }\n\n private async initialize(): Promise<void> {\n try {\n // Fetch initial flags from API\n await this.fetchFlags();\n\n // Set up real-time updates if enabled\n if (this.config.realtimeEnabled) {\n await this.setupRealtime();\n }\n\n this.status = 'ready';\n this.initResolve();\n this.log('Client initialized');\n } catch (err) {\n this.status = 'error';\n this.error = err instanceof Error ? err : new Error(String(err));\n this.initReject(this.error);\n this.log('Initialization failed:', this.error.message);\n }\n }\n\n private async fetchFlags(): Promise<void> {\n const url = `${this.config.baseUrl}/api/sdk/flags`;\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch flags: ${response.status}`);\n }\n\n const data = (await response.json()) as { flags: FlagMap };\n this.flags = data.flags;\n this.cacheFlags();\n this.notifyListeners();\n this.log('Flags fetched:', Object.keys(this.flags).length);\n }\n\n private async setupRealtime(): Promise<void> {\n // Initialize Firebase with minimal config for Realtime DB\n // The database URL is derived from the API response or configured\n const databaseURL = await this.getDatabaseUrl();\n\n if (!databaseURL) {\n this.log('Realtime DB URL not available, skipping real-time updates');\n\n return;\n }\n\n this.firebaseApp = initializeApp(\n {\n databaseURL,\n },\n `rolloutly-${Date.now()}`,\n );\n\n this.database = getDatabase(this.firebaseApp);\n\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n\n this.realtimeUnsubscribe = onValue(\n flagsRef,\n (snapshot) => {\n const data = snapshot.val() as Record<string, Flag> | null;\n\n if (data) {\n // Convert realtime format to our flag format\n this.flags = Object.entries(data).reduce<FlagMap>(\n (acc, [key, flag]) => {\n acc[key] = { ...flag, key };\n\n return acc;\n },\n {},\n );\n this.cacheFlags();\n this.notifyListeners();\n this.log('Realtime update received');\n }\n },\n (error) => {\n this.log('Realtime error:', error.message);\n },\n );\n }\n\n private async getDatabaseUrl(): Promise<string | null> {\n // Try to get the database URL from the API\n try {\n const url = `${this.config.baseUrl}/api/sdk/config`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (response.ok) {\n const data = (await response.json()) as { databaseUrl?: string };\n\n return data.databaseUrl ?? null;\n }\n } catch {\n // Ignore - we'll try without realtime\n }\n\n return null;\n }\n\n private loadCachedFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const cached = localStorage.getItem(CACHE_KEY);\n\n if (cached) {\n this.flags = JSON.parse(cached) as FlagMap;\n this.log('Loaded cached flags');\n }\n } catch {\n // Ignore cache errors\n }\n }\n\n private cacheFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));\n } catch {\n // Ignore cache errors\n }\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => listener());\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Rolloutly]', ...args);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { initializeApp, type FirebaseApp } from 'firebase/app';\nimport {\n getDatabase,\n off,\n onValue,\n ref,\n type Database,\n type Unsubscribe,\n} from 'firebase/database';\n\nimport type {\n ClientStatus,\n Flag,\n FlagChangeListener,\n FlagMap,\n FlagValue,\n ParsedToken,\n RolloutlyConfig,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://rolloutly.com';\nconst CACHE_KEY = 'rolloutly_flags';\n\n/**\n * Convert a string to camelCase\n * 'instagram-integration' -> 'instagramIntegration'\n * 'my_feature_flag' -> 'myFeatureFlag'\n */\nconst toCamelCase = (str: string): string => {\n return str.replace(/[-_](.)/g, (_, char: string) => char.toUpperCase());\n};\n\n/**\n * Check if a key needs camelCase conversion (has hyphens or underscores)\n */\nconst needsCamelCase = (key: string): boolean => {\n return key.includes('-') || key.includes('_');\n};\n\nexport class RolloutlyClient {\n private config: Required<\n Omit<RolloutlyConfig, 'defaultFlags'> & {\n defaultFlags: Record<string, FlagValue>;\n }\n >;\n private flags: FlagMap = {};\n private flagValuesCache: Record<string, FlagValue | undefined> = {};\n private status: ClientStatus = 'initializing';\n private error: Error | null = null;\n private listeners: Set<FlagChangeListener> = new Set();\n private parsedToken: ParsedToken;\n private firebaseApp: FirebaseApp | null = null;\n private database: Database | null = null;\n private realtimeUnsubscribe: Unsubscribe | null = null;\n private initPromise: Promise<void>;\n private initResolve!: () => void;\n private initReject!: (error: Error) => void;\n\n constructor(config: RolloutlyConfig) {\n this.config = {\n token: config.token,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n realtimeEnabled: config.realtimeEnabled ?? true,\n defaultFlags: config.defaultFlags ?? {},\n debug: config.debug ?? false,\n };\n\n // Parse the token\n const parsed = this.parseToken(config.token);\n\n if (!parsed) {\n throw new Error('Invalid SDK token format');\n }\n\n this.parsedToken = parsed;\n\n // Create init promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve;\n this.initReject = reject;\n });\n\n // Load cached flags first\n this.loadCachedFlags();\n\n // Start initialization\n void this.initialize();\n }\n\n /**\n * Wait for the client to be initialized\n */\n async waitForInit(): Promise<void> {\n return this.initPromise;\n }\n\n /**\n * Get a single flag value\n * Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')\n */\n getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined {\n // Try direct lookup first\n let flag = this.flags[key];\n\n // If not found and key is camelCase, try to find original key\n if (!flag) {\n const originalKey = this.findOriginalKey(key);\n\n if (originalKey) {\n flag = this.flags[originalKey];\n }\n }\n\n if (flag) {\n const defaultKey = this.findOriginalKey(key) || key;\n\n return (\n flag.enabled ? flag.value : this.config.defaultFlags[defaultKey]\n ) as T;\n }\n\n return this.config.defaultFlags[key] as T;\n }\n\n /**\n * Get all flags\n */\n getFlags(): FlagMap {\n return { ...this.flags };\n }\n\n /**\n * Get all flag values as a stable object (for React hooks)\n * Returns a cached object that only changes when flags change\n */\n getFlagValues(): Record<string, FlagValue | undefined> {\n return this.flagValuesCache;\n }\n\n /**\n * Check if a boolean flag is enabled\n * Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')\n */\n isEnabled(key: string): boolean {\n // Try direct lookup first\n let flag = this.flags[key];\n let lookupKey = key;\n\n // If not found and key is camelCase, try to find original key\n if (!flag) {\n const originalKey = this.findOriginalKey(key);\n\n if (originalKey) {\n flag = this.flags[originalKey];\n lookupKey = originalKey;\n }\n }\n\n if (!flag) {\n return Boolean(this.config.defaultFlags[lookupKey]);\n }\n\n if (!flag.enabled) {\n return Boolean(this.config.defaultFlags[lookupKey]);\n }\n\n return flag.type === 'boolean' ? Boolean(flag.value) : true;\n }\n\n /**\n * Get current client status\n */\n getStatus(): ClientStatus {\n return this.status;\n }\n\n /**\n * Get the last error if any\n */\n getError(): Error | null {\n return this.error;\n }\n\n /**\n * Subscribe to flag changes (for React useSyncExternalStore)\n */\n subscribe(listener: FlagChangeListener): () => void {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Cleanup and disconnect\n */\n close(): void {\n if (this.realtimeUnsubscribe) {\n this.realtimeUnsubscribe();\n this.realtimeUnsubscribe = null;\n }\n\n if (this.database) {\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n off(flagsRef);\n }\n\n this.listeners.clear();\n this.log('Client closed');\n }\n\n // ==================== Private Methods ====================\n\n /**\n * Find the original flag key from a camelCase version\n * 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)\n */\n private findOriginalKey(camelKey: string): string | null {\n // If the key exists directly, return null (no conversion needed)\n if (this.flags[camelKey]) {\n return null;\n }\n\n // Search for a flag whose camelCase version matches\n for (const key of Object.keys(this.flags)) {\n if (needsCamelCase(key) && toCamelCase(key) === camelKey) {\n return key;\n }\n }\n\n return null;\n }\n\n private parseToken(token: string): ParsedToken | null {\n const parts = token.split('_');\n\n // Format: rly_{projectId}_{environmentKey}_{randomString}\n if (parts.length < 4 || parts[0] !== 'rly') {\n return null;\n }\n\n return {\n projectId: parts[1],\n environmentKey: parts[2],\n };\n }\n\n private async initialize(): Promise<void> {\n try {\n // Fetch initial flags from API\n await this.fetchFlags();\n\n // Set up real-time updates if enabled\n if (this.config.realtimeEnabled) {\n await this.setupRealtime();\n }\n\n this.status = 'ready';\n this.initResolve();\n this.log('Client initialized');\n } catch (err) {\n this.status = 'error';\n this.error = err instanceof Error ? err : new Error(String(err));\n this.initReject(this.error);\n this.log('Initialization failed:', this.error.message);\n }\n }\n\n private async fetchFlags(): Promise<void> {\n const url = `${this.config.baseUrl}/api/sdk/flags`;\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch flags: ${response.status}`);\n }\n\n const data = (await response.json()) as { flags: FlagMap };\n this.flags = data.flags;\n this.cacheFlags();\n this.notifyListeners();\n this.log('Flags fetched:', Object.keys(this.flags).length);\n }\n\n private async setupRealtime(): Promise<void> {\n // Initialize Firebase with minimal config for Realtime DB\n // The database URL is derived from the API response or configured\n const databaseURL = await this.getDatabaseUrl();\n\n if (!databaseURL) {\n this.log('Realtime DB URL not available, skipping real-time updates');\n\n return;\n }\n\n this.firebaseApp = initializeApp(\n {\n databaseURL,\n },\n `rolloutly-${Date.now()}`,\n );\n\n this.database = getDatabase(this.firebaseApp);\n\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n\n this.realtimeUnsubscribe = onValue(\n flagsRef,\n (snapshot) => {\n const data = snapshot.val() as Record<string, Flag> | null;\n\n if (data) {\n // Convert realtime format to our flag format\n this.flags = Object.entries(data).reduce<FlagMap>(\n (acc, [key, flag]) => {\n acc[key] = { ...flag, key };\n\n return acc;\n },\n {},\n );\n this.cacheFlags();\n this.notifyListeners();\n this.log('Realtime update received');\n }\n },\n (error) => {\n this.log('Realtime error:', error.message);\n },\n );\n }\n\n private async getDatabaseUrl(): Promise<string | null> {\n // Try to get the database URL from the API\n try {\n const url = `${this.config.baseUrl}/api/sdk/config`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (response.ok) {\n const data = (await response.json()) as { databaseUrl?: string };\n\n return data.databaseUrl ?? null;\n }\n } catch {\n // Ignore - we'll try without realtime\n }\n\n return null;\n }\n\n private loadCachedFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const cached = localStorage.getItem(CACHE_KEY);\n\n if (cached) {\n this.flags = JSON.parse(cached) as FlagMap;\n this.updateFlagValuesCache();\n this.log('Loaded cached flags');\n }\n } catch {\n // Ignore cache errors\n }\n }\n\n private cacheFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));\n } catch {\n // Ignore cache errors\n }\n }\n\n private updateFlagValuesCache(): void {\n // Create a new cache object only when flags change\n // Include both original keys and camelCase versions for easy destructuring\n this.flagValuesCache = Object.entries(this.flags).reduce<\n Record<string, FlagValue | undefined>\n >((acc, [key, flag]) => {\n // Always include original key\n acc[key] = flag.value;\n\n // If key has hyphens or underscores, also add camelCase version\n if (needsCamelCase(key)) {\n const camelKey = toCamelCase(key);\n acc[camelKey] = flag.value;\n }\n\n return acc;\n }, {});\n }\n\n private notifyListeners(): void {\n this.updateFlagValuesCache();\n this.listeners.forEach((listener) => listener());\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Rolloutly]', ...args);\n }\n }\n}\n"]}
|