@rolloutly/core 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/index.cjs +48 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +48 -12
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -62,11 +62,70 @@ const client = new RolloutlyClient({
|
|
|
62
62
|
'api-rate-limit': 100,
|
|
63
63
|
},
|
|
64
64
|
|
|
65
|
+
// Optional: User context for targeting rules
|
|
66
|
+
user: {
|
|
67
|
+
userId: 'user-123',
|
|
68
|
+
email: 'alice@example.com',
|
|
69
|
+
orgId: 'acme-corp',
|
|
70
|
+
plan: 'pro',
|
|
71
|
+
},
|
|
72
|
+
|
|
65
73
|
// Optional: Enable debug logging (default: false)
|
|
66
74
|
debug: true,
|
|
67
75
|
});
|
|
68
76
|
```
|
|
69
77
|
|
|
78
|
+
## User Targeting
|
|
79
|
+
|
|
80
|
+
Pass user context to enable personalized flag values based on targeting rules.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const client = new RolloutlyClient({
|
|
84
|
+
token: 'rly_xxx',
|
|
85
|
+
user: {
|
|
86
|
+
userId: 'user-123',
|
|
87
|
+
email: 'alice@example.com',
|
|
88
|
+
orgId: 'acme-corp',
|
|
89
|
+
plan: 'pro',
|
|
90
|
+
role: 'admin',
|
|
91
|
+
// Custom attributes
|
|
92
|
+
betaUser: true,
|
|
93
|
+
signupDate: '2024-01-15',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await client.waitForInit();
|
|
98
|
+
|
|
99
|
+
// Flags are personalized based on targeting rules
|
|
100
|
+
if (client.isEnabled('premium-feature')) {
|
|
101
|
+
// Show premium feature
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Update user context (e.g., after login)
|
|
105
|
+
await client.identify({
|
|
106
|
+
userId: 'user-456',
|
|
107
|
+
email: 'bob@example.com',
|
|
108
|
+
plan: 'enterprise',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Clear user context (e.g., on logout)
|
|
112
|
+
await client.reset();
|
|
113
|
+
|
|
114
|
+
// Get current user context
|
|
115
|
+
const currentUser = client.getUser();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### User Context Properties
|
|
119
|
+
|
|
120
|
+
| Property | Type | Description |
|
|
121
|
+
|----------|------|-------------|
|
|
122
|
+
| `userId` | `string` | Unique user identifier |
|
|
123
|
+
| `email` | `string` | User's email address |
|
|
124
|
+
| `orgId` | `string` | Organization/company ID |
|
|
125
|
+
| `plan` | `string` | Subscription plan |
|
|
126
|
+
| `role` | `string` | User's role |
|
|
127
|
+
| `[key]` | `string \| number \| boolean \| string[]` | Custom attributes |
|
|
128
|
+
|
|
70
129
|
## API Reference
|
|
71
130
|
|
|
72
131
|
### `RolloutlyClient`
|
|
@@ -80,6 +139,9 @@ const client = new RolloutlyClient({
|
|
|
80
139
|
- `getStatus(): ClientStatus` - Get client status ('initializing' | 'ready' | 'error')
|
|
81
140
|
- `getError(): Error | null` - Get the last error
|
|
82
141
|
- `subscribe(listener: () => void): () => void` - Subscribe to flag changes
|
|
142
|
+
- `identify(user: UserContext): Promise<void>` - Update user context and re-fetch flags
|
|
143
|
+
- `reset(): Promise<void>` - Clear user context and re-fetch flags
|
|
144
|
+
- `getUser(): UserContext | undefined` - Get current user context
|
|
83
145
|
- `close(): void` - Cleanup and disconnect
|
|
84
146
|
|
|
85
147
|
## For React
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ var RolloutlyClient = class {
|
|
|
27
27
|
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
28
28
|
realtimeEnabled: config.realtimeEnabled ?? true,
|
|
29
29
|
defaultFlags: config.defaultFlags ?? {},
|
|
30
|
+
user: config.user,
|
|
30
31
|
debug: config.debug ?? false
|
|
31
32
|
};
|
|
32
33
|
const parsed = this.parseToken(config.token);
|
|
@@ -121,6 +122,29 @@ var RolloutlyClient = class {
|
|
|
121
122
|
this.listeners.delete(listener);
|
|
122
123
|
};
|
|
123
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Update the user context and re-fetch flags
|
|
127
|
+
* Call this when the user logs in or their attributes change
|
|
128
|
+
*/
|
|
129
|
+
async identify(user) {
|
|
130
|
+
this.config.user = user;
|
|
131
|
+
this.log("User identified:", user.userId || user.email || "anonymous");
|
|
132
|
+
await this.fetchFlags();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Clear the user context (e.g., on logout)
|
|
136
|
+
*/
|
|
137
|
+
async reset() {
|
|
138
|
+
this.config.user = void 0;
|
|
139
|
+
this.log("User context reset");
|
|
140
|
+
await this.fetchFlags();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the current user context
|
|
144
|
+
*/
|
|
145
|
+
getUser() {
|
|
146
|
+
return this.config.user;
|
|
147
|
+
}
|
|
124
148
|
/**
|
|
125
149
|
* Cleanup and disconnect
|
|
126
150
|
*/
|
|
@@ -183,9 +207,15 @@ var RolloutlyClient = class {
|
|
|
183
207
|
}
|
|
184
208
|
async fetchFlags() {
|
|
185
209
|
const url = `${this.config.baseUrl}/api/sdk/flags`;
|
|
210
|
+
const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;
|
|
186
211
|
const response = await fetch(url, {
|
|
212
|
+
method: hasUserContext ? "POST" : "GET",
|
|
187
213
|
headers: {
|
|
188
|
-
Authorization: `Bearer ${this.config.token}
|
|
214
|
+
Authorization: `Bearer ${this.config.token}`,
|
|
215
|
+
...hasUserContext && { "Content-Type": "application/json" }
|
|
216
|
+
},
|
|
217
|
+
...hasUserContext && {
|
|
218
|
+
body: JSON.stringify({ user: this.config.user })
|
|
189
219
|
}
|
|
190
220
|
});
|
|
191
221
|
if (!response.ok) {
|
|
@@ -195,7 +225,7 @@ var RolloutlyClient = class {
|
|
|
195
225
|
this.flags = data.flags;
|
|
196
226
|
this.cacheFlags();
|
|
197
227
|
this.notifyListeners();
|
|
198
|
-
this.log("Flags fetched:", Object.keys(this.flags).length);
|
|
228
|
+
this.log("Flags fetched:", Object.keys(this.flags).length, hasUserContext ? "(with user context)" : "");
|
|
199
229
|
}
|
|
200
230
|
async setupRealtime() {
|
|
201
231
|
const databaseURL = await this.getDatabaseUrl();
|
|
@@ -219,16 +249,22 @@ var RolloutlyClient = class {
|
|
|
219
249
|
(snapshot) => {
|
|
220
250
|
const data = snapshot.val();
|
|
221
251
|
if (data) {
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;
|
|
253
|
+
if (hasUserContext) {
|
|
254
|
+
this.log("Realtime change detected, re-fetching with user context...");
|
|
255
|
+
void this.fetchFlags();
|
|
256
|
+
} else {
|
|
257
|
+
this.flags = Object.entries(data).reduce(
|
|
258
|
+
(acc, [key, flag]) => {
|
|
259
|
+
acc[key] = { ...flag, key };
|
|
260
|
+
return acc;
|
|
261
|
+
},
|
|
262
|
+
{}
|
|
263
|
+
);
|
|
264
|
+
this.cacheFlags();
|
|
265
|
+
this.notifyListeners();
|
|
266
|
+
this.log("Realtime update received");
|
|
267
|
+
}
|
|
232
268
|
}
|
|
233
269
|
},
|
|
234
270
|
(error) => {
|
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 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":["ref","off","initializeApp","getDatabase","onValue"],"mappings":";;;;;;AAqBA,IAAM,gBAAA,GAAmB,uBAAA;AACzB,IAAM,SAAA,GAAY,iBAAA;AAOlB,IAAM,WAAA,GAAc,CAAC,GAAA,KAAwB;AAC3C,EAAA,OAAO,GAAA,CAAI,QAAQ,UAAA,EAAY,CAAC,GAAG,IAAA,KAAiB,IAAA,CAAK,aAAa,CAAA;AACxE,CAAA;AAKA,IAAM,cAAA,GAAiB,CAAC,GAAA,KAAyB;AAC/C,EAAA,OAAO,IAAI,QAAA,CAAS,GAAG,CAAA,IAAK,GAAA,CAAI,SAAS,GAAG,CAAA;AAC9C,CAAA;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAoB3B,YAAY,MAAA,EAAyB;AAbrC,IAAA,IAAA,CAAQ,QAAiB,EAAC;AAC1B,IAAA,IAAA,CAAQ,kBAAyD,EAAC;AAClE,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,MAAM,MAAA,CAAO,IAAA;AAAA,MACb,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;AAAA,EAMA,QAAyC,GAAA,EAA4B;AAEnE,IAAA,IAAI,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAGzB,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AAE5C,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,IAAA,GAAO,IAAA,CAAK,MAAM,WAAW,CAAA;AAAA,MAC/B;AAAA,IACF;AAEA,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,MAAM,UAAA,GAAa,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA,IAAK,GAAA;AAEhD,MAAA,OACE,KAAK,OAAA,GAAU,IAAA,CAAK,QAAQ,IAAA,CAAK,MAAA,CAAO,aAAa,UAAU,CAAA;AAAA,IAEnE;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;AAAA,EAMA,aAAA,GAAuD;AACrD,IAAA,OAAO,IAAA,CAAK,eAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,GAAA,EAAsB;AAE9B,IAAA,IAAI,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AACzB,IAAA,IAAI,SAAA,GAAY,GAAA;AAGhB,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AAE5C,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,IAAA,GAAO,IAAA,CAAK,MAAM,WAAW,CAAA;AAC7B,QAAA,SAAA,GAAY,WAAA;AAAA,MACd;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,SAAS,CAAC,CAAA;AAAA,IACpD;AAEA,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACjB,MAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,SAAS,CAAC,CAAA;AAAA,IACpD;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;AAAA,EAMA,MAAM,SAAS,IAAA,EAAkC;AAC/C,IAAA,IAAA,CAAK,OAAO,IAAA,GAAO,IAAA;AACnB,IAAA,IAAA,CAAK,IAAI,kBAAA,EAAoB,IAAA,CAAK,MAAA,IAAU,IAAA,CAAK,SAAS,WAAW,CAAA;AAGrE,IAAA,MAAM,KAAK,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAA,CAAK,OAAO,IAAA,GAAO,MAAA;AACnB,IAAA,IAAA,CAAK,IAAI,oBAAoB,CAAA;AAG7B,IAAA,MAAM,KAAK,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAA,GAAmC;AACjC,IAAA,OAAO,KAAK,MAAA,CAAO,IAAA;AAAA,EACrB;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,GAAWA,YAAA;AAAA,QACf,IAAA,CAAK,QAAA;AAAA,QACL,SAAS,IAAA,CAAK,WAAA,CAAY,SAAS,CAAA,CAAA,EAAI,IAAA,CAAK,YAAY,cAAc,CAAA;AAAA,OACxE;AACA,MAAAC,YAAA,CAAI,QAAQ,CAAA;AAAA,IACd;AAEA,IAAA,IAAA,CAAK,UAAU,KAAA,EAAM;AACrB,IAAA,IAAA,CAAK,IAAI,eAAe,CAAA;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,gBAAgB,QAAA,EAAiC;AAEvD,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA,EAAG;AACxB,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,EAAG;AACzC,MAAA,IAAI,eAAe,GAAG,CAAA,IAAK,WAAA,CAAY,GAAG,MAAM,QAAA,EAAU;AACxD,QAAA,OAAO,GAAA;AAAA,MACT;AAAA,IACF;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEQ,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;AAGlC,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,MAAA,CAAO,IAAA,IAAQ,MAAA,CAAO,KAAK,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA,GAAS,CAAA;AAElF,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,MAChC,MAAA,EAAQ,iBAAiB,MAAA,GAAS,KAAA;AAAA,MAClC,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA,CAAA;AAAA,QAC1C,GAAI,cAAA,IAAkB,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC7D;AAAA,MACA,GAAI,cAAA,IAAkB;AAAA,QACpB,IAAA,EAAM,KAAK,SAAA,CAAU,EAAE,MAAM,IAAA,CAAK,MAAA,CAAO,MAAM;AAAA;AACjD,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,GAAA,CAAI,gBAAA,EAAkB,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,CAAE,MAAA,EAAQ,cAAA,GAAiB,qBAAA,GAAwB,EAAE,CAAA;AAAA,EACxG;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,GAAcC,iBAAA;AAAA,MACjB;AAAA,QACE;AAAA,OACF;AAAA,MACA,CAAA,UAAA,EAAa,IAAA,CAAK,GAAA,EAAK,CAAA;AAAA,KACzB;AAEA,IAAA,IAAA,CAAK,QAAA,GAAWC,oBAAA,CAAY,IAAA,CAAK,WAAW,CAAA;AAE5C,IAAA,MAAM,QAAA,GAAWH,YAAA;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,GAAsBI,gBAAA;AAAA,MACzB,QAAA;AAAA,MACA,CAAC,QAAA,KAAa;AACZ,QAAA,MAAM,IAAA,GAAO,SAAS,GAAA,EAAI;AAE1B,QAAA,IAAI,IAAA,EAAM;AACR,UAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,MAAA,CAAO,IAAA,IAAQ,MAAA,CAAO,KAAK,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA,GAAS,CAAA;AAElF,UAAA,IAAI,cAAA,EAAgB;AAGlB,YAAA,IAAA,CAAK,IAAI,4DAA4D,CAAA;AACrE,YAAA,KAAK,KAAK,UAAA,EAAW;AAAA,UACvB,CAAA,MAAO;AAEL,YAAA,IAAA,CAAK,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,CAAE,MAAA;AAAA,cAChC,CAAC,GAAA,EAAK,CAAC,GAAA,EAAK,IAAI,CAAA,KAAM;AACpB,gBAAA,GAAA,CAAI,GAAG,CAAA,GAAI,EAAE,GAAG,MAAM,GAAA,EAAI;AAE1B,gBAAA,OAAO,GAAA;AAAA,cACT,CAAA;AAAA,cACA;AAAC,aACH;AACA,YAAA,IAAA,CAAK,UAAA,EAAW;AAChB,YAAA,IAAA,CAAK,eAAA,EAAgB;AACrB,YAAA,IAAA,CAAK,IAAI,0BAA0B,CAAA;AAAA,UACrC;AAAA,QACF;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,qBAAA,EAAsB;AAC3B,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,qBAAA,GAA8B;AAGpC,IAAA,IAAA,CAAK,eAAA,GAAkB,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,CAAE,MAAA,CAEhD,CAAC,GAAA,EAAK,CAAC,GAAA,EAAK,IAAI,CAAA,KAAM;AAEtB,MAAA,GAAA,CAAI,GAAG,IAAI,IAAA,CAAK,KAAA;AAGhB,MAAA,IAAI,cAAA,CAAe,GAAG,CAAA,EAAG;AACvB,QAAA,MAAM,QAAA,GAAW,YAAY,GAAG,CAAA;AAChC,QAAA,GAAA,CAAI,QAAQ,IAAI,IAAA,CAAK,KAAA;AAAA,MACvB;AAEA,MAAA,OAAO,GAAA;AAAA,IACT,CAAA,EAAG,EAAE,CAAA;AAAA,EACP;AAAA,EAEQ,eAAA,GAAwB;AAC9B,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,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.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 UserContext,\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' | 'user'> & {\n defaultFlags: Record<string, FlagValue>;\n user: UserContext | undefined;\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 user: config.user,\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 * Update the user context and re-fetch flags\n * Call this when the user logs in or their attributes change\n */\n async identify(user: UserContext): Promise<void> {\n this.config.user = user;\n this.log('User identified:', user.userId || user.email || 'anonymous');\n\n // Re-fetch flags with new user context\n await this.fetchFlags();\n }\n\n /**\n * Clear the user context (e.g., on logout)\n */\n async reset(): Promise<void> {\n this.config.user = undefined;\n this.log('User context reset');\n\n // Re-fetch flags without user context\n await this.fetchFlags();\n }\n\n /**\n * Get the current user context\n */\n getUser(): UserContext | undefined {\n return this.config.user;\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 // Use POST with user context if available, otherwise GET\n const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;\n\n const response = await fetch(url, {\n method: hasUserContext ? 'POST' : 'GET',\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n ...(hasUserContext && { 'Content-Type': 'application/json' }),\n },\n ...(hasUserContext && {\n body: JSON.stringify({ user: this.config.user }),\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, hasUserContext ? '(with user context)' : '');\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 const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;\n\n if (hasUserContext) {\n // When user context is provided, re-fetch from API to get\n // server-evaluated targeting rules instead of using raw values\n this.log('Realtime change detected, re-fetching with user context...');\n void this.fetchFlags();\n } else {\n // No user context, use realtime values directly\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 },\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
|
@@ -15,6 +15,24 @@ type Flag = {
|
|
|
15
15
|
* Map of flag keys to their data
|
|
16
16
|
*/
|
|
17
17
|
type FlagMap = Record<string, Flag>;
|
|
18
|
+
/**
|
|
19
|
+
* User context for targeting rules evaluation
|
|
20
|
+
* Pass user properties to enable personalized flag values
|
|
21
|
+
*/
|
|
22
|
+
type UserContext = {
|
|
23
|
+
/** Unique user identifier */
|
|
24
|
+
userId?: string;
|
|
25
|
+
/** User's email address */
|
|
26
|
+
email?: string;
|
|
27
|
+
/** Organization/company identifier */
|
|
28
|
+
orgId?: string;
|
|
29
|
+
/** User's subscription plan (e.g., 'free', 'pro', 'enterprise') */
|
|
30
|
+
plan?: string;
|
|
31
|
+
/** User's role in the application */
|
|
32
|
+
role?: string;
|
|
33
|
+
/** Custom attributes for targeting */
|
|
34
|
+
[key: string]: string | number | boolean | string[] | undefined;
|
|
35
|
+
};
|
|
18
36
|
/**
|
|
19
37
|
* Configuration options for RolloutlyClient
|
|
20
38
|
*/
|
|
@@ -27,6 +45,8 @@ type RolloutlyConfig = {
|
|
|
27
45
|
realtimeEnabled?: boolean;
|
|
28
46
|
/** Default flag values to use before flags are loaded */
|
|
29
47
|
defaultFlags?: Record<string, FlagValue>;
|
|
48
|
+
/** User context for targeting rules evaluation */
|
|
49
|
+
user?: UserContext;
|
|
30
50
|
/** Enable debug logging (default: false) */
|
|
31
51
|
debug?: boolean;
|
|
32
52
|
};
|
|
@@ -89,6 +109,19 @@ declare class RolloutlyClient {
|
|
|
89
109
|
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
90
110
|
*/
|
|
91
111
|
subscribe(listener: FlagChangeListener): () => void;
|
|
112
|
+
/**
|
|
113
|
+
* Update the user context and re-fetch flags
|
|
114
|
+
* Call this when the user logs in or their attributes change
|
|
115
|
+
*/
|
|
116
|
+
identify(user: UserContext): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Clear the user context (e.g., on logout)
|
|
119
|
+
*/
|
|
120
|
+
reset(): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Get the current user context
|
|
123
|
+
*/
|
|
124
|
+
getUser(): UserContext | undefined;
|
|
92
125
|
/**
|
|
93
126
|
* Cleanup and disconnect
|
|
94
127
|
*/
|
|
@@ -110,4 +143,4 @@ declare class RolloutlyClient {
|
|
|
110
143
|
private log;
|
|
111
144
|
}
|
|
112
145
|
|
|
113
|
-
export { type ClientStatus, type Flag, type FlagChangeListener, type FlagMap, type FlagValue, RolloutlyClient, type RolloutlyConfig };
|
|
146
|
+
export { type ClientStatus, type Flag, type FlagChangeListener, type FlagMap, type FlagValue, RolloutlyClient, type RolloutlyConfig, type UserContext };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,24 @@ type Flag = {
|
|
|
15
15
|
* Map of flag keys to their data
|
|
16
16
|
*/
|
|
17
17
|
type FlagMap = Record<string, Flag>;
|
|
18
|
+
/**
|
|
19
|
+
* User context for targeting rules evaluation
|
|
20
|
+
* Pass user properties to enable personalized flag values
|
|
21
|
+
*/
|
|
22
|
+
type UserContext = {
|
|
23
|
+
/** Unique user identifier */
|
|
24
|
+
userId?: string;
|
|
25
|
+
/** User's email address */
|
|
26
|
+
email?: string;
|
|
27
|
+
/** Organization/company identifier */
|
|
28
|
+
orgId?: string;
|
|
29
|
+
/** User's subscription plan (e.g., 'free', 'pro', 'enterprise') */
|
|
30
|
+
plan?: string;
|
|
31
|
+
/** User's role in the application */
|
|
32
|
+
role?: string;
|
|
33
|
+
/** Custom attributes for targeting */
|
|
34
|
+
[key: string]: string | number | boolean | string[] | undefined;
|
|
35
|
+
};
|
|
18
36
|
/**
|
|
19
37
|
* Configuration options for RolloutlyClient
|
|
20
38
|
*/
|
|
@@ -27,6 +45,8 @@ type RolloutlyConfig = {
|
|
|
27
45
|
realtimeEnabled?: boolean;
|
|
28
46
|
/** Default flag values to use before flags are loaded */
|
|
29
47
|
defaultFlags?: Record<string, FlagValue>;
|
|
48
|
+
/** User context for targeting rules evaluation */
|
|
49
|
+
user?: UserContext;
|
|
30
50
|
/** Enable debug logging (default: false) */
|
|
31
51
|
debug?: boolean;
|
|
32
52
|
};
|
|
@@ -89,6 +109,19 @@ declare class RolloutlyClient {
|
|
|
89
109
|
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
90
110
|
*/
|
|
91
111
|
subscribe(listener: FlagChangeListener): () => void;
|
|
112
|
+
/**
|
|
113
|
+
* Update the user context and re-fetch flags
|
|
114
|
+
* Call this when the user logs in or their attributes change
|
|
115
|
+
*/
|
|
116
|
+
identify(user: UserContext): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Clear the user context (e.g., on logout)
|
|
119
|
+
*/
|
|
120
|
+
reset(): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Get the current user context
|
|
123
|
+
*/
|
|
124
|
+
getUser(): UserContext | undefined;
|
|
92
125
|
/**
|
|
93
126
|
* Cleanup and disconnect
|
|
94
127
|
*/
|
|
@@ -110,4 +143,4 @@ declare class RolloutlyClient {
|
|
|
110
143
|
private log;
|
|
111
144
|
}
|
|
112
145
|
|
|
113
|
-
export { type ClientStatus, type Flag, type FlagChangeListener, type FlagMap, type FlagValue, RolloutlyClient, type RolloutlyConfig };
|
|
146
|
+
export { type ClientStatus, type Flag, type FlagChangeListener, type FlagMap, type FlagValue, RolloutlyClient, type RolloutlyConfig, type UserContext };
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ var RolloutlyClient = class {
|
|
|
25
25
|
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
26
26
|
realtimeEnabled: config.realtimeEnabled ?? true,
|
|
27
27
|
defaultFlags: config.defaultFlags ?? {},
|
|
28
|
+
user: config.user,
|
|
28
29
|
debug: config.debug ?? false
|
|
29
30
|
};
|
|
30
31
|
const parsed = this.parseToken(config.token);
|
|
@@ -119,6 +120,29 @@ var RolloutlyClient = class {
|
|
|
119
120
|
this.listeners.delete(listener);
|
|
120
121
|
};
|
|
121
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Update the user context and re-fetch flags
|
|
125
|
+
* Call this when the user logs in or their attributes change
|
|
126
|
+
*/
|
|
127
|
+
async identify(user) {
|
|
128
|
+
this.config.user = user;
|
|
129
|
+
this.log("User identified:", user.userId || user.email || "anonymous");
|
|
130
|
+
await this.fetchFlags();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Clear the user context (e.g., on logout)
|
|
134
|
+
*/
|
|
135
|
+
async reset() {
|
|
136
|
+
this.config.user = void 0;
|
|
137
|
+
this.log("User context reset");
|
|
138
|
+
await this.fetchFlags();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the current user context
|
|
142
|
+
*/
|
|
143
|
+
getUser() {
|
|
144
|
+
return this.config.user;
|
|
145
|
+
}
|
|
122
146
|
/**
|
|
123
147
|
* Cleanup and disconnect
|
|
124
148
|
*/
|
|
@@ -181,9 +205,15 @@ var RolloutlyClient = class {
|
|
|
181
205
|
}
|
|
182
206
|
async fetchFlags() {
|
|
183
207
|
const url = `${this.config.baseUrl}/api/sdk/flags`;
|
|
208
|
+
const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;
|
|
184
209
|
const response = await fetch(url, {
|
|
210
|
+
method: hasUserContext ? "POST" : "GET",
|
|
185
211
|
headers: {
|
|
186
|
-
Authorization: `Bearer ${this.config.token}
|
|
212
|
+
Authorization: `Bearer ${this.config.token}`,
|
|
213
|
+
...hasUserContext && { "Content-Type": "application/json" }
|
|
214
|
+
},
|
|
215
|
+
...hasUserContext && {
|
|
216
|
+
body: JSON.stringify({ user: this.config.user })
|
|
187
217
|
}
|
|
188
218
|
});
|
|
189
219
|
if (!response.ok) {
|
|
@@ -193,7 +223,7 @@ var RolloutlyClient = class {
|
|
|
193
223
|
this.flags = data.flags;
|
|
194
224
|
this.cacheFlags();
|
|
195
225
|
this.notifyListeners();
|
|
196
|
-
this.log("Flags fetched:", Object.keys(this.flags).length);
|
|
226
|
+
this.log("Flags fetched:", Object.keys(this.flags).length, hasUserContext ? "(with user context)" : "");
|
|
197
227
|
}
|
|
198
228
|
async setupRealtime() {
|
|
199
229
|
const databaseURL = await this.getDatabaseUrl();
|
|
@@ -217,16 +247,22 @@ var RolloutlyClient = class {
|
|
|
217
247
|
(snapshot) => {
|
|
218
248
|
const data = snapshot.val();
|
|
219
249
|
if (data) {
|
|
220
|
-
this.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
250
|
+
const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;
|
|
251
|
+
if (hasUserContext) {
|
|
252
|
+
this.log("Realtime change detected, re-fetching with user context...");
|
|
253
|
+
void this.fetchFlags();
|
|
254
|
+
} else {
|
|
255
|
+
this.flags = Object.entries(data).reduce(
|
|
256
|
+
(acc, [key, flag]) => {
|
|
257
|
+
acc[key] = { ...flag, key };
|
|
258
|
+
return acc;
|
|
259
|
+
},
|
|
260
|
+
{}
|
|
261
|
+
);
|
|
262
|
+
this.cacheFlags();
|
|
263
|
+
this.notifyListeners();
|
|
264
|
+
this.log("Realtime update received");
|
|
265
|
+
}
|
|
230
266
|
}
|
|
231
267
|
},
|
|
232
268
|
(error) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":";;;;AAqBA,IAAM,gBAAA,GAAmB,uBAAA;AACzB,IAAM,SAAA,GAAY,iBAAA;AAOlB,IAAM,WAAA,GAAc,CAAC,GAAA,KAAwB;AAC3C,EAAA,OAAO,GAAA,CAAI,QAAQ,UAAA,EAAY,CAAC,GAAG,IAAA,KAAiB,IAAA,CAAK,aAAa,CAAA;AACxE,CAAA;AAKA,IAAM,cAAA,GAAiB,CAAC,GAAA,KAAyB;AAC/C,EAAA,OAAO,IAAI,QAAA,CAAS,GAAG,CAAA,IAAK,GAAA,CAAI,SAAS,GAAG,CAAA;AAC9C,CAAA;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAoB3B,YAAY,MAAA,EAAyB;AAbrC,IAAA,IAAA,CAAQ,QAAiB,EAAC;AAC1B,IAAA,IAAA,CAAQ,kBAAyD,EAAC;AAClE,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,MAAM,MAAA,CAAO,IAAA;AAAA,MACb,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;AAAA,EAMA,QAAyC,GAAA,EAA4B;AAEnE,IAAA,IAAI,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAGzB,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AAE5C,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,IAAA,GAAO,IAAA,CAAK,MAAM,WAAW,CAAA;AAAA,MAC/B;AAAA,IACF;AAEA,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,MAAM,UAAA,GAAa,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA,IAAK,GAAA;AAEhD,MAAA,OACE,KAAK,OAAA,GAAU,IAAA,CAAK,QAAQ,IAAA,CAAK,MAAA,CAAO,aAAa,UAAU,CAAA;AAAA,IAEnE;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;AAAA,EAMA,aAAA,GAAuD;AACrD,IAAA,OAAO,IAAA,CAAK,eAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,GAAA,EAAsB;AAE9B,IAAA,IAAI,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AACzB,IAAA,IAAI,SAAA,GAAY,GAAA;AAGhB,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,WAAA,GAAc,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AAE5C,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,IAAA,GAAO,IAAA,CAAK,MAAM,WAAW,CAAA;AAC7B,QAAA,SAAA,GAAY,WAAA;AAAA,MACd;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,SAAS,CAAC,CAAA;AAAA,IACpD;AAEA,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACjB,MAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,SAAS,CAAC,CAAA;AAAA,IACpD;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;AAAA,EAMA,MAAM,SAAS,IAAA,EAAkC;AAC/C,IAAA,IAAA,CAAK,OAAO,IAAA,GAAO,IAAA;AACnB,IAAA,IAAA,CAAK,IAAI,kBAAA,EAAoB,IAAA,CAAK,MAAA,IAAU,IAAA,CAAK,SAAS,WAAW,CAAA;AAGrE,IAAA,MAAM,KAAK,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAA,CAAK,OAAO,IAAA,GAAO,MAAA;AACnB,IAAA,IAAA,CAAK,IAAI,oBAAoB,CAAA;AAG7B,IAAA,MAAM,KAAK,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAA,GAAmC;AACjC,IAAA,OAAO,KAAK,MAAA,CAAO,IAAA;AAAA,EACrB;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;AAAA;AAAA;AAAA;AAAA,EAQQ,gBAAgB,QAAA,EAAiC;AAEvD,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA,EAAG;AACxB,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,EAAG;AACzC,MAAA,IAAI,eAAe,GAAG,CAAA,IAAK,WAAA,CAAY,GAAG,MAAM,QAAA,EAAU;AACxD,QAAA,OAAO,GAAA;AAAA,MACT;AAAA,IACF;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEQ,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;AAGlC,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,MAAA,CAAO,IAAA,IAAQ,MAAA,CAAO,KAAK,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA,GAAS,CAAA;AAElF,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,MAChC,MAAA,EAAQ,iBAAiB,MAAA,GAAS,KAAA;AAAA,MAClC,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA,CAAA;AAAA,QAC1C,GAAI,cAAA,IAAkB,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC7D;AAAA,MACA,GAAI,cAAA,IAAkB;AAAA,QACpB,IAAA,EAAM,KAAK,SAAA,CAAU,EAAE,MAAM,IAAA,CAAK,MAAA,CAAO,MAAM;AAAA;AACjD,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,GAAA,CAAI,gBAAA,EAAkB,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,CAAE,MAAA,EAAQ,cAAA,GAAiB,qBAAA,GAAwB,EAAE,CAAA;AAAA,EACxG;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;AACR,UAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,MAAA,CAAO,IAAA,IAAQ,MAAA,CAAO,KAAK,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA,GAAS,CAAA;AAElF,UAAA,IAAI,cAAA,EAAgB;AAGlB,YAAA,IAAA,CAAK,IAAI,4DAA4D,CAAA;AACrE,YAAA,KAAK,KAAK,UAAA,EAAW;AAAA,UACvB,CAAA,MAAO;AAEL,YAAA,IAAA,CAAK,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,CAAE,MAAA;AAAA,cAChC,CAAC,GAAA,EAAK,CAAC,GAAA,EAAK,IAAI,CAAA,KAAM;AACpB,gBAAA,GAAA,CAAI,GAAG,CAAA,GAAI,EAAE,GAAG,MAAM,GAAA,EAAI;AAE1B,gBAAA,OAAO,GAAA;AAAA,cACT,CAAA;AAAA,cACA;AAAC,aACH;AACA,YAAA,IAAA,CAAK,UAAA,EAAW;AAChB,YAAA,IAAA,CAAK,eAAA,EAAgB;AACrB,YAAA,IAAA,CAAK,IAAI,0BAA0B,CAAA;AAAA,UACrC;AAAA,QACF;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,qBAAA,EAAsB;AAC3B,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,qBAAA,GAA8B;AAGpC,IAAA,IAAA,CAAK,eAAA,GAAkB,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,CAAE,MAAA,CAEhD,CAAC,GAAA,EAAK,CAAC,GAAA,EAAK,IAAI,CAAA,KAAM;AAEtB,MAAA,GAAA,CAAI,GAAG,IAAI,IAAA,CAAK,KAAA;AAGhB,MAAA,IAAI,cAAA,CAAe,GAAG,CAAA,EAAG;AACvB,QAAA,MAAM,QAAA,GAAW,YAAY,GAAG,CAAA;AAChC,QAAA,GAAA,CAAI,QAAQ,IAAI,IAAA,CAAK,KAAA;AAAA,MACvB;AAEA,MAAA,OAAO,GAAA;AAAA,IACT,CAAA,EAAG,EAAE,CAAA;AAAA,EACP;AAAA,EAEQ,eAAA,GAAwB;AAC9B,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,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 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 UserContext,\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' | 'user'> & {\n defaultFlags: Record<string, FlagValue>;\n user: UserContext | undefined;\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 user: config.user,\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 * Update the user context and re-fetch flags\n * Call this when the user logs in or their attributes change\n */\n async identify(user: UserContext): Promise<void> {\n this.config.user = user;\n this.log('User identified:', user.userId || user.email || 'anonymous');\n\n // Re-fetch flags with new user context\n await this.fetchFlags();\n }\n\n /**\n * Clear the user context (e.g., on logout)\n */\n async reset(): Promise<void> {\n this.config.user = undefined;\n this.log('User context reset');\n\n // Re-fetch flags without user context\n await this.fetchFlags();\n }\n\n /**\n * Get the current user context\n */\n getUser(): UserContext | undefined {\n return this.config.user;\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 // Use POST with user context if available, otherwise GET\n const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;\n\n const response = await fetch(url, {\n method: hasUserContext ? 'POST' : 'GET',\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n ...(hasUserContext && { 'Content-Type': 'application/json' }),\n },\n ...(hasUserContext && {\n body: JSON.stringify({ user: this.config.user }),\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, hasUserContext ? '(with user context)' : '');\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 const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;\n\n if (hasUserContext) {\n // When user context is provided, re-fetch from API to get\n // server-evaluated targeting rules instead of using raw values\n this.log('Realtime change detected, re-fetching with user context...');\n void this.fetchFlags();\n } else {\n // No user context, use realtime values directly\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 },\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rolloutly/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Rolloutly feature flags SDK - Core JavaScript client",
|
|
5
5
|
"author": "Kevin Beltrão",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,17 +35,17 @@
|
|
|
35
35
|
"dist",
|
|
36
36
|
"README.md"
|
|
37
37
|
],
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsup",
|
|
40
|
-
"dev": "tsup --watch",
|
|
41
|
-
"typecheck": "tsc --noEmit",
|
|
42
|
-
"clean": "rm -rf dist"
|
|
43
|
-
},
|
|
44
38
|
"dependencies": {
|
|
45
39
|
"firebase": "^10.7.0"
|
|
46
40
|
},
|
|
47
41
|
"devDependencies": {
|
|
48
42
|
"tsup": "^8.0.1",
|
|
49
43
|
"typescript": "^5.3.3"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"clean": "rm -rf dist"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|