@rolloutly/core 0.1.2 → 0.1.4
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 +95 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +95 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
@@ -6,6 +6,12 @@ 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 = {};
|
|
@@ -21,6 +27,7 @@ var RolloutlyClient = class {
|
|
|
21
27
|
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
22
28
|
realtimeEnabled: config.realtimeEnabled ?? true,
|
|
23
29
|
defaultFlags: config.defaultFlags ?? {},
|
|
30
|
+
user: config.user,
|
|
24
31
|
debug: config.debug ?? false
|
|
25
32
|
};
|
|
26
33
|
const parsed = this.parseToken(config.token);
|
|
@@ -43,11 +50,19 @@ var RolloutlyClient = class {
|
|
|
43
50
|
}
|
|
44
51
|
/**
|
|
45
52
|
* Get a single flag value
|
|
53
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
46
54
|
*/
|
|
47
55
|
getFlag(key) {
|
|
48
|
-
|
|
56
|
+
let flag = this.flags[key];
|
|
57
|
+
if (!flag) {
|
|
58
|
+
const originalKey = this.findOriginalKey(key);
|
|
59
|
+
if (originalKey) {
|
|
60
|
+
flag = this.flags[originalKey];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
49
63
|
if (flag) {
|
|
50
|
-
|
|
64
|
+
const defaultKey = this.findOriginalKey(key) || key;
|
|
65
|
+
return flag.enabled ? flag.value : this.config.defaultFlags[defaultKey];
|
|
51
66
|
}
|
|
52
67
|
return this.config.defaultFlags[key];
|
|
53
68
|
}
|
|
@@ -66,14 +81,23 @@ var RolloutlyClient = class {
|
|
|
66
81
|
}
|
|
67
82
|
/**
|
|
68
83
|
* Check if a boolean flag is enabled
|
|
84
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
69
85
|
*/
|
|
70
86
|
isEnabled(key) {
|
|
71
|
-
|
|
87
|
+
let flag = this.flags[key];
|
|
88
|
+
let lookupKey = key;
|
|
89
|
+
if (!flag) {
|
|
90
|
+
const originalKey = this.findOriginalKey(key);
|
|
91
|
+
if (originalKey) {
|
|
92
|
+
flag = this.flags[originalKey];
|
|
93
|
+
lookupKey = originalKey;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
72
96
|
if (!flag) {
|
|
73
|
-
return Boolean(this.config.defaultFlags[
|
|
97
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
74
98
|
}
|
|
75
99
|
if (!flag.enabled) {
|
|
76
|
-
return Boolean(this.config.defaultFlags[
|
|
100
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
77
101
|
}
|
|
78
102
|
return flag.type === "boolean" ? Boolean(flag.value) : true;
|
|
79
103
|
}
|
|
@@ -98,6 +122,29 @@ var RolloutlyClient = class {
|
|
|
98
122
|
this.listeners.delete(listener);
|
|
99
123
|
};
|
|
100
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
|
+
}
|
|
101
148
|
/**
|
|
102
149
|
* Cleanup and disconnect
|
|
103
150
|
*/
|
|
@@ -117,6 +164,21 @@ var RolloutlyClient = class {
|
|
|
117
164
|
this.log("Client closed");
|
|
118
165
|
}
|
|
119
166
|
// ==================== Private Methods ====================
|
|
167
|
+
/**
|
|
168
|
+
* Find the original flag key from a camelCase version
|
|
169
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
170
|
+
*/
|
|
171
|
+
findOriginalKey(camelKey) {
|
|
172
|
+
if (this.flags[camelKey]) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
for (const key of Object.keys(this.flags)) {
|
|
176
|
+
if (needsCamelCase(key) && toCamelCase(key) === camelKey) {
|
|
177
|
+
return key;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
120
182
|
parseToken(token) {
|
|
121
183
|
const parts = token.split("_");
|
|
122
184
|
if (parts.length < 4 || parts[0] !== "rly") {
|
|
@@ -145,9 +207,15 @@ var RolloutlyClient = class {
|
|
|
145
207
|
}
|
|
146
208
|
async fetchFlags() {
|
|
147
209
|
const url = `${this.config.baseUrl}/api/sdk/flags`;
|
|
210
|
+
const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;
|
|
148
211
|
const response = await fetch(url, {
|
|
212
|
+
method: hasUserContext ? "POST" : "GET",
|
|
149
213
|
headers: {
|
|
150
|
-
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 })
|
|
151
219
|
}
|
|
152
220
|
});
|
|
153
221
|
if (!response.ok) {
|
|
@@ -157,7 +225,7 @@ var RolloutlyClient = class {
|
|
|
157
225
|
this.flags = data.flags;
|
|
158
226
|
this.cacheFlags();
|
|
159
227
|
this.notifyListeners();
|
|
160
|
-
this.log("Flags fetched:", Object.keys(this.flags).length);
|
|
228
|
+
this.log("Flags fetched:", Object.keys(this.flags).length, hasUserContext ? "(with user context)" : "");
|
|
161
229
|
}
|
|
162
230
|
async setupRealtime() {
|
|
163
231
|
const databaseURL = await this.getDatabaseUrl();
|
|
@@ -181,16 +249,22 @@ var RolloutlyClient = class {
|
|
|
181
249
|
(snapshot) => {
|
|
182
250
|
const data = snapshot.val();
|
|
183
251
|
if (data) {
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
}
|
|
194
268
|
}
|
|
195
269
|
},
|
|
196
270
|
(error) => {
|
|
@@ -236,6 +310,10 @@ var RolloutlyClient = class {
|
|
|
236
310
|
updateFlagValuesCache() {
|
|
237
311
|
this.flagValuesCache = Object.entries(this.flags).reduce((acc, [key, flag]) => {
|
|
238
312
|
acc[key] = flag.value;
|
|
313
|
+
if (needsCamelCase(key)) {
|
|
314
|
+
const camelKey = toCamelCase(key);
|
|
315
|
+
acc[camelKey] = flag.value;
|
|
316
|
+
}
|
|
239
317
|
return acc;
|
|
240
318
|
}, {});
|
|
241
319
|
}
|
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\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 */\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 * 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 */\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.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 this.flagValuesCache = Object.entries(this.flags).reduce<\n Record<string, FlagValue | undefined>\n >((acc, [key, flag]) => {\n acc[key] = flag.value;\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":"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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 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
|
};
|
|
@@ -60,6 +80,7 @@ declare class RolloutlyClient {
|
|
|
60
80
|
waitForInit(): Promise<void>;
|
|
61
81
|
/**
|
|
62
82
|
* Get a single flag value
|
|
83
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
63
84
|
*/
|
|
64
85
|
getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined;
|
|
65
86
|
/**
|
|
@@ -73,6 +94,7 @@ declare class RolloutlyClient {
|
|
|
73
94
|
getFlagValues(): Record<string, FlagValue | undefined>;
|
|
74
95
|
/**
|
|
75
96
|
* Check if a boolean flag is enabled
|
|
97
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
76
98
|
*/
|
|
77
99
|
isEnabled(key: string): boolean;
|
|
78
100
|
/**
|
|
@@ -87,10 +109,28 @@ declare class RolloutlyClient {
|
|
|
87
109
|
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
88
110
|
*/
|
|
89
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;
|
|
90
125
|
/**
|
|
91
126
|
* Cleanup and disconnect
|
|
92
127
|
*/
|
|
93
128
|
close(): void;
|
|
129
|
+
/**
|
|
130
|
+
* Find the original flag key from a camelCase version
|
|
131
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
132
|
+
*/
|
|
133
|
+
private findOriginalKey;
|
|
94
134
|
private parseToken;
|
|
95
135
|
private initialize;
|
|
96
136
|
private fetchFlags;
|
|
@@ -103,4 +143,4 @@ declare class RolloutlyClient {
|
|
|
103
143
|
private log;
|
|
104
144
|
}
|
|
105
145
|
|
|
106
|
-
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
|
};
|
|
@@ -60,6 +80,7 @@ declare class RolloutlyClient {
|
|
|
60
80
|
waitForInit(): Promise<void>;
|
|
61
81
|
/**
|
|
62
82
|
* Get a single flag value
|
|
83
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
63
84
|
*/
|
|
64
85
|
getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined;
|
|
65
86
|
/**
|
|
@@ -73,6 +94,7 @@ declare class RolloutlyClient {
|
|
|
73
94
|
getFlagValues(): Record<string, FlagValue | undefined>;
|
|
74
95
|
/**
|
|
75
96
|
* Check if a boolean flag is enabled
|
|
97
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
76
98
|
*/
|
|
77
99
|
isEnabled(key: string): boolean;
|
|
78
100
|
/**
|
|
@@ -87,10 +109,28 @@ declare class RolloutlyClient {
|
|
|
87
109
|
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
88
110
|
*/
|
|
89
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;
|
|
90
125
|
/**
|
|
91
126
|
* Cleanup and disconnect
|
|
92
127
|
*/
|
|
93
128
|
close(): void;
|
|
129
|
+
/**
|
|
130
|
+
* Find the original flag key from a camelCase version
|
|
131
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
132
|
+
*/
|
|
133
|
+
private findOriginalKey;
|
|
94
134
|
private parseToken;
|
|
95
135
|
private initialize;
|
|
96
136
|
private fetchFlags;
|
|
@@ -103,4 +143,4 @@ declare class RolloutlyClient {
|
|
|
103
143
|
private log;
|
|
104
144
|
}
|
|
105
145
|
|
|
106
|
-
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
|
@@ -4,6 +4,12 @@ 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 = {};
|
|
@@ -19,6 +25,7 @@ var RolloutlyClient = class {
|
|
|
19
25
|
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
20
26
|
realtimeEnabled: config.realtimeEnabled ?? true,
|
|
21
27
|
defaultFlags: config.defaultFlags ?? {},
|
|
28
|
+
user: config.user,
|
|
22
29
|
debug: config.debug ?? false
|
|
23
30
|
};
|
|
24
31
|
const parsed = this.parseToken(config.token);
|
|
@@ -41,11 +48,19 @@ var RolloutlyClient = class {
|
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* Get a single flag value
|
|
51
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
44
52
|
*/
|
|
45
53
|
getFlag(key) {
|
|
46
|
-
|
|
54
|
+
let flag = this.flags[key];
|
|
55
|
+
if (!flag) {
|
|
56
|
+
const originalKey = this.findOriginalKey(key);
|
|
57
|
+
if (originalKey) {
|
|
58
|
+
flag = this.flags[originalKey];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
47
61
|
if (flag) {
|
|
48
|
-
|
|
62
|
+
const defaultKey = this.findOriginalKey(key) || key;
|
|
63
|
+
return flag.enabled ? flag.value : this.config.defaultFlags[defaultKey];
|
|
49
64
|
}
|
|
50
65
|
return this.config.defaultFlags[key];
|
|
51
66
|
}
|
|
@@ -64,14 +79,23 @@ var RolloutlyClient = class {
|
|
|
64
79
|
}
|
|
65
80
|
/**
|
|
66
81
|
* Check if a boolean flag is enabled
|
|
82
|
+
* Supports both original keys ('instagram-integration') and camelCase ('instagramIntegration')
|
|
67
83
|
*/
|
|
68
84
|
isEnabled(key) {
|
|
69
|
-
|
|
85
|
+
let flag = this.flags[key];
|
|
86
|
+
let lookupKey = key;
|
|
87
|
+
if (!flag) {
|
|
88
|
+
const originalKey = this.findOriginalKey(key);
|
|
89
|
+
if (originalKey) {
|
|
90
|
+
flag = this.flags[originalKey];
|
|
91
|
+
lookupKey = originalKey;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
70
94
|
if (!flag) {
|
|
71
|
-
return Boolean(this.config.defaultFlags[
|
|
95
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
72
96
|
}
|
|
73
97
|
if (!flag.enabled) {
|
|
74
|
-
return Boolean(this.config.defaultFlags[
|
|
98
|
+
return Boolean(this.config.defaultFlags[lookupKey]);
|
|
75
99
|
}
|
|
76
100
|
return flag.type === "boolean" ? Boolean(flag.value) : true;
|
|
77
101
|
}
|
|
@@ -96,6 +120,29 @@ var RolloutlyClient = class {
|
|
|
96
120
|
this.listeners.delete(listener);
|
|
97
121
|
};
|
|
98
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
|
+
}
|
|
99
146
|
/**
|
|
100
147
|
* Cleanup and disconnect
|
|
101
148
|
*/
|
|
@@ -115,6 +162,21 @@ var RolloutlyClient = class {
|
|
|
115
162
|
this.log("Client closed");
|
|
116
163
|
}
|
|
117
164
|
// ==================== Private Methods ====================
|
|
165
|
+
/**
|
|
166
|
+
* Find the original flag key from a camelCase version
|
|
167
|
+
* 'instagramIntegration' -> 'instagram-integration' (if it exists in flags)
|
|
168
|
+
*/
|
|
169
|
+
findOriginalKey(camelKey) {
|
|
170
|
+
if (this.flags[camelKey]) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
for (const key of Object.keys(this.flags)) {
|
|
174
|
+
if (needsCamelCase(key) && toCamelCase(key) === camelKey) {
|
|
175
|
+
return key;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
118
180
|
parseToken(token) {
|
|
119
181
|
const parts = token.split("_");
|
|
120
182
|
if (parts.length < 4 || parts[0] !== "rly") {
|
|
@@ -143,9 +205,15 @@ var RolloutlyClient = class {
|
|
|
143
205
|
}
|
|
144
206
|
async fetchFlags() {
|
|
145
207
|
const url = `${this.config.baseUrl}/api/sdk/flags`;
|
|
208
|
+
const hasUserContext = this.config.user && Object.keys(this.config.user).length > 0;
|
|
146
209
|
const response = await fetch(url, {
|
|
210
|
+
method: hasUserContext ? "POST" : "GET",
|
|
147
211
|
headers: {
|
|
148
|
-
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 })
|
|
149
217
|
}
|
|
150
218
|
});
|
|
151
219
|
if (!response.ok) {
|
|
@@ -155,7 +223,7 @@ var RolloutlyClient = class {
|
|
|
155
223
|
this.flags = data.flags;
|
|
156
224
|
this.cacheFlags();
|
|
157
225
|
this.notifyListeners();
|
|
158
|
-
this.log("Flags fetched:", Object.keys(this.flags).length);
|
|
226
|
+
this.log("Flags fetched:", Object.keys(this.flags).length, hasUserContext ? "(with user context)" : "");
|
|
159
227
|
}
|
|
160
228
|
async setupRealtime() {
|
|
161
229
|
const databaseURL = await this.getDatabaseUrl();
|
|
@@ -179,16 +247,22 @@ var RolloutlyClient = class {
|
|
|
179
247
|
(snapshot) => {
|
|
180
248
|
const data = snapshot.val();
|
|
181
249
|
if (data) {
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
}
|
|
192
266
|
}
|
|
193
267
|
},
|
|
194
268
|
(error) => {
|
|
@@ -234,6 +308,10 @@ var RolloutlyClient = class {
|
|
|
234
308
|
updateFlagValuesCache() {
|
|
235
309
|
this.flagValuesCache = Object.entries(this.flags).reduce((acc, [key, flag]) => {
|
|
236
310
|
acc[key] = flag.value;
|
|
311
|
+
if (needsCamelCase(key)) {
|
|
312
|
+
const camelKey = toCamelCase(key);
|
|
313
|
+
acc[camelKey] = flag.value;
|
|
314
|
+
}
|
|
237
315
|
return acc;
|
|
238
316
|
}, {});
|
|
239
317
|
}
|
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\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 */\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 * 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 */\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.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 this.flagValuesCache = Object.entries(this.flags).reduce<\n Record<string, FlagValue | undefined>\n >((acc, [key, flag]) => {\n acc[key] = flag.value;\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":"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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 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"]}
|