@rolloutly/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -0
- package/dist/index.cjs +239 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +99 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +237 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @rolloutly/core
|
|
2
|
+
|
|
3
|
+
Core JavaScript SDK for [Rolloutly](https://rolloutly.com) feature flags.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @rolloutly/core
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @rolloutly/core
|
|
11
|
+
# or
|
|
12
|
+
yarn add @rolloutly/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { RolloutlyClient } from '@rolloutly/core';
|
|
19
|
+
|
|
20
|
+
// Initialize the client
|
|
21
|
+
const client = new RolloutlyClient({
|
|
22
|
+
token: 'rly_your_project_production_xxx',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Wait for initialization
|
|
26
|
+
await client.waitForInit();
|
|
27
|
+
|
|
28
|
+
// Get a flag value
|
|
29
|
+
const showNewFeature = client.isEnabled('new-feature');
|
|
30
|
+
|
|
31
|
+
// Get a typed flag value
|
|
32
|
+
const rateLimit = client.getFlag<number>('api-rate-limit');
|
|
33
|
+
|
|
34
|
+
// Get all flags
|
|
35
|
+
const flags = client.getFlags();
|
|
36
|
+
|
|
37
|
+
// Subscribe to changes
|
|
38
|
+
const unsubscribe = client.subscribe(() => {
|
|
39
|
+
console.log('Flags updated!');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Cleanup when done
|
|
43
|
+
client.close();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const client = new RolloutlyClient({
|
|
50
|
+
// Required: Your SDK token
|
|
51
|
+
token: 'rly_xxx',
|
|
52
|
+
|
|
53
|
+
// Optional: Custom API URL (default: https://rolloutly.com)
|
|
54
|
+
baseUrl: 'https://your-instance.com',
|
|
55
|
+
|
|
56
|
+
// Optional: Enable real-time updates (default: true)
|
|
57
|
+
realtimeEnabled: true,
|
|
58
|
+
|
|
59
|
+
// Optional: Default values before flags load
|
|
60
|
+
defaultFlags: {
|
|
61
|
+
'new-feature': false,
|
|
62
|
+
'api-rate-limit': 100,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Optional: Enable debug logging (default: false)
|
|
66
|
+
debug: true,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### `RolloutlyClient`
|
|
73
|
+
|
|
74
|
+
#### Methods
|
|
75
|
+
|
|
76
|
+
- `waitForInit(): Promise<void>` - Wait for the client to initialize
|
|
77
|
+
- `getFlag<T>(key: string): T | undefined` - Get a single flag value
|
|
78
|
+
- `getFlags(): FlagMap` - Get all flags
|
|
79
|
+
- `isEnabled(key: string): boolean` - Check if a boolean flag is enabled
|
|
80
|
+
- `getStatus(): ClientStatus` - Get client status ('initializing' | 'ready' | 'error')
|
|
81
|
+
- `getError(): Error | null` - Get the last error
|
|
82
|
+
- `subscribe(listener: () => void): () => void` - Subscribe to flag changes
|
|
83
|
+
- `close(): void` - Cleanup and disconnect
|
|
84
|
+
|
|
85
|
+
## For React
|
|
86
|
+
|
|
87
|
+
For React applications, use `@rolloutly/react` which provides hooks and a provider:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm install @rolloutly/react
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See [@rolloutly/react](../react) for more information.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var app = require('firebase/app');
|
|
4
|
+
var database = require('firebase/database');
|
|
5
|
+
|
|
6
|
+
// src/client.ts
|
|
7
|
+
var DEFAULT_BASE_URL = "https://rolloutly.com";
|
|
8
|
+
var CACHE_KEY = "rolloutly_flags";
|
|
9
|
+
var RolloutlyClient = class {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.flags = {};
|
|
12
|
+
this.status = "initializing";
|
|
13
|
+
this.error = null;
|
|
14
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
15
|
+
this.firebaseApp = null;
|
|
16
|
+
this.database = null;
|
|
17
|
+
this.realtimeUnsubscribe = null;
|
|
18
|
+
this.config = {
|
|
19
|
+
token: config.token,
|
|
20
|
+
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
21
|
+
realtimeEnabled: config.realtimeEnabled ?? true,
|
|
22
|
+
defaultFlags: config.defaultFlags ?? {},
|
|
23
|
+
debug: config.debug ?? false
|
|
24
|
+
};
|
|
25
|
+
const parsed = this.parseToken(config.token);
|
|
26
|
+
if (!parsed) {
|
|
27
|
+
throw new Error("Invalid SDK token format");
|
|
28
|
+
}
|
|
29
|
+
this.parsedToken = parsed;
|
|
30
|
+
this.initPromise = new Promise((resolve, reject) => {
|
|
31
|
+
this.initResolve = resolve;
|
|
32
|
+
this.initReject = reject;
|
|
33
|
+
});
|
|
34
|
+
this.loadCachedFlags();
|
|
35
|
+
void this.initialize();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wait for the client to be initialized
|
|
39
|
+
*/
|
|
40
|
+
async waitForInit() {
|
|
41
|
+
return this.initPromise;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get a single flag value
|
|
45
|
+
*/
|
|
46
|
+
getFlag(key) {
|
|
47
|
+
const flag = this.flags[key];
|
|
48
|
+
if (flag) {
|
|
49
|
+
return flag.enabled ? flag.value : this.config.defaultFlags[key];
|
|
50
|
+
}
|
|
51
|
+
return this.config.defaultFlags[key];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get all flags
|
|
55
|
+
*/
|
|
56
|
+
getFlags() {
|
|
57
|
+
return { ...this.flags };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if a boolean flag is enabled
|
|
61
|
+
*/
|
|
62
|
+
isEnabled(key) {
|
|
63
|
+
const flag = this.flags[key];
|
|
64
|
+
if (!flag) {
|
|
65
|
+
return Boolean(this.config.defaultFlags[key]);
|
|
66
|
+
}
|
|
67
|
+
if (!flag.enabled) {
|
|
68
|
+
return Boolean(this.config.defaultFlags[key]);
|
|
69
|
+
}
|
|
70
|
+
return flag.type === "boolean" ? Boolean(flag.value) : true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get current client status
|
|
74
|
+
*/
|
|
75
|
+
getStatus() {
|
|
76
|
+
return this.status;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get the last error if any
|
|
80
|
+
*/
|
|
81
|
+
getError() {
|
|
82
|
+
return this.error;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
86
|
+
*/
|
|
87
|
+
subscribe(listener) {
|
|
88
|
+
this.listeners.add(listener);
|
|
89
|
+
return () => {
|
|
90
|
+
this.listeners.delete(listener);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Cleanup and disconnect
|
|
95
|
+
*/
|
|
96
|
+
close() {
|
|
97
|
+
if (this.realtimeUnsubscribe) {
|
|
98
|
+
this.realtimeUnsubscribe();
|
|
99
|
+
this.realtimeUnsubscribe = null;
|
|
100
|
+
}
|
|
101
|
+
if (this.database) {
|
|
102
|
+
const flagsRef = database.ref(
|
|
103
|
+
this.database,
|
|
104
|
+
`flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`
|
|
105
|
+
);
|
|
106
|
+
database.off(flagsRef);
|
|
107
|
+
}
|
|
108
|
+
this.listeners.clear();
|
|
109
|
+
this.log("Client closed");
|
|
110
|
+
}
|
|
111
|
+
// ==================== Private Methods ====================
|
|
112
|
+
parseToken(token) {
|
|
113
|
+
const parts = token.split("_");
|
|
114
|
+
if (parts.length < 4 || parts[0] !== "rly") {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
projectId: parts[1],
|
|
119
|
+
environmentKey: parts[2]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async initialize() {
|
|
123
|
+
try {
|
|
124
|
+
await this.fetchFlags();
|
|
125
|
+
if (this.config.realtimeEnabled) {
|
|
126
|
+
await this.setupRealtime();
|
|
127
|
+
}
|
|
128
|
+
this.status = "ready";
|
|
129
|
+
this.initResolve();
|
|
130
|
+
this.log("Client initialized");
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.status = "error";
|
|
133
|
+
this.error = err instanceof Error ? err : new Error(String(err));
|
|
134
|
+
this.initReject(this.error);
|
|
135
|
+
this.log("Initialization failed:", this.error.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async fetchFlags() {
|
|
139
|
+
const url = `${this.config.baseUrl}/api/sdk/flags`;
|
|
140
|
+
const response = await fetch(url, {
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${this.config.token}`
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`Failed to fetch flags: ${response.status}`);
|
|
147
|
+
}
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
this.flags = data.flags;
|
|
150
|
+
this.cacheFlags();
|
|
151
|
+
this.notifyListeners();
|
|
152
|
+
this.log("Flags fetched:", Object.keys(this.flags).length);
|
|
153
|
+
}
|
|
154
|
+
async setupRealtime() {
|
|
155
|
+
const databaseURL = await this.getDatabaseUrl();
|
|
156
|
+
if (!databaseURL) {
|
|
157
|
+
this.log("Realtime DB URL not available, skipping real-time updates");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.firebaseApp = app.initializeApp(
|
|
161
|
+
{
|
|
162
|
+
databaseURL
|
|
163
|
+
},
|
|
164
|
+
`rolloutly-${Date.now()}`
|
|
165
|
+
);
|
|
166
|
+
this.database = database.getDatabase(this.firebaseApp);
|
|
167
|
+
const flagsRef = database.ref(
|
|
168
|
+
this.database,
|
|
169
|
+
`flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`
|
|
170
|
+
);
|
|
171
|
+
this.realtimeUnsubscribe = database.onValue(
|
|
172
|
+
flagsRef,
|
|
173
|
+
(snapshot) => {
|
|
174
|
+
const data = snapshot.val();
|
|
175
|
+
if (data) {
|
|
176
|
+
this.flags = Object.entries(data).reduce(
|
|
177
|
+
(acc, [key, flag]) => {
|
|
178
|
+
acc[key] = { ...flag, key };
|
|
179
|
+
return acc;
|
|
180
|
+
},
|
|
181
|
+
{}
|
|
182
|
+
);
|
|
183
|
+
this.cacheFlags();
|
|
184
|
+
this.notifyListeners();
|
|
185
|
+
this.log("Realtime update received");
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
(error) => {
|
|
189
|
+
this.log("Realtime error:", error.message);
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
async getDatabaseUrl() {
|
|
194
|
+
try {
|
|
195
|
+
const url = `${this.config.baseUrl}/api/sdk/config`;
|
|
196
|
+
const response = await fetch(url, {
|
|
197
|
+
headers: {
|
|
198
|
+
Authorization: `Bearer ${this.config.token}`
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
if (response.ok) {
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
return data.databaseUrl ?? null;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
loadCachedFlags() {
|
|
210
|
+
if (typeof window === "undefined") return;
|
|
211
|
+
try {
|
|
212
|
+
const cached = localStorage.getItem(CACHE_KEY);
|
|
213
|
+
if (cached) {
|
|
214
|
+
this.flags = JSON.parse(cached);
|
|
215
|
+
this.log("Loaded cached flags");
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
cacheFlags() {
|
|
221
|
+
if (typeof window === "undefined") return;
|
|
222
|
+
try {
|
|
223
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
notifyListeners() {
|
|
228
|
+
this.listeners.forEach((listener) => listener());
|
|
229
|
+
}
|
|
230
|
+
log(...args) {
|
|
231
|
+
if (this.config.debug) {
|
|
232
|
+
console.log("[Rolloutly]", ...args);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
exports.RolloutlyClient = RolloutlyClient;
|
|
238
|
+
//# sourceMappingURL=index.cjs.map
|
|
239
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":["ref","off","initializeApp","getDatabase","onValue"],"mappings":"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","file":"index.cjs","sourcesContent":["import { initializeApp, type FirebaseApp } from 'firebase/app';\nimport {\n getDatabase,\n ref,\n onValue,\n off,\n type Database,\n type Unsubscribe,\n} from 'firebase/database';\n\nimport type {\n ClientStatus,\n Flag,\n FlagChangeListener,\n FlagMap,\n FlagValue,\n ParsedToken,\n RolloutlyConfig,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://rolloutly.com';\nconst CACHE_KEY = 'rolloutly_flags';\n\nexport class RolloutlyClient {\n private config: Required<\n Omit<RolloutlyConfig, 'defaultFlags'> & {\n defaultFlags: Record<string, FlagValue>;\n }\n >;\n private flags: FlagMap = {};\n private status: ClientStatus = 'initializing';\n private error: Error | null = null;\n private listeners: Set<FlagChangeListener> = new Set();\n private parsedToken: ParsedToken;\n private firebaseApp: FirebaseApp | null = null;\n private database: Database | null = null;\n private realtimeUnsubscribe: Unsubscribe | null = null;\n private initPromise: Promise<void>;\n private initResolve!: () => void;\n private initReject!: (error: Error) => void;\n\n constructor(config: RolloutlyConfig) {\n this.config = {\n token: config.token,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n realtimeEnabled: config.realtimeEnabled ?? true,\n defaultFlags: config.defaultFlags ?? {},\n debug: config.debug ?? false,\n };\n\n // Parse the token\n const parsed = this.parseToken(config.token);\n\n if (!parsed) {\n throw new Error('Invalid SDK token format');\n }\n\n this.parsedToken = parsed;\n\n // Create init promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve;\n this.initReject = reject;\n });\n\n // Load cached flags first\n this.loadCachedFlags();\n\n // Start initialization\n void this.initialize();\n }\n\n /**\n * Wait for the client to be initialized\n */\n async waitForInit(): Promise<void> {\n return this.initPromise;\n }\n\n /**\n * Get a single flag value\n */\n getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined {\n const flag = this.flags[key];\n\n if (flag) {\n return (flag.enabled ? flag.value : this.config.defaultFlags[key]) as T;\n }\n\n return this.config.defaultFlags[key] as T;\n }\n\n /**\n * Get all flags\n */\n getFlags(): FlagMap {\n return { ...this.flags };\n }\n\n /**\n * Check if a boolean flag is enabled\n */\n isEnabled(key: string): boolean {\n const flag = this.flags[key];\n\n if (!flag) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n if (!flag.enabled) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n return flag.type === 'boolean' ? Boolean(flag.value) : true;\n }\n\n /**\n * Get current client status\n */\n getStatus(): ClientStatus {\n return this.status;\n }\n\n /**\n * Get the last error if any\n */\n getError(): Error | null {\n return this.error;\n }\n\n /**\n * Subscribe to flag changes (for React useSyncExternalStore)\n */\n subscribe(listener: FlagChangeListener): () => void {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Cleanup and disconnect\n */\n close(): void {\n if (this.realtimeUnsubscribe) {\n this.realtimeUnsubscribe();\n this.realtimeUnsubscribe = null;\n }\n\n if (this.database) {\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n off(flagsRef);\n }\n\n this.listeners.clear();\n this.log('Client closed');\n }\n\n // ==================== Private Methods ====================\n\n private parseToken(token: string): ParsedToken | null {\n const parts = token.split('_');\n\n // Format: rly_{projectId}_{environmentKey}_{randomString}\n if (parts.length < 4 || parts[0] !== 'rly') {\n return null;\n }\n\n return {\n projectId: parts[1],\n environmentKey: parts[2],\n };\n }\n\n private async initialize(): Promise<void> {\n try {\n // Fetch initial flags from API\n await this.fetchFlags();\n\n // Set up real-time updates if enabled\n if (this.config.realtimeEnabled) {\n await this.setupRealtime();\n }\n\n this.status = 'ready';\n this.initResolve();\n this.log('Client initialized');\n } catch (err) {\n this.status = 'error';\n this.error = err instanceof Error ? err : new Error(String(err));\n this.initReject(this.error);\n this.log('Initialization failed:', this.error.message);\n }\n }\n\n private async fetchFlags(): Promise<void> {\n const url = `${this.config.baseUrl}/api/sdk/flags`;\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch flags: ${response.status}`);\n }\n\n const data = (await response.json()) as { flags: FlagMap };\n this.flags = data.flags;\n this.cacheFlags();\n this.notifyListeners();\n this.log('Flags fetched:', Object.keys(this.flags).length);\n }\n\n private async setupRealtime(): Promise<void> {\n // Initialize Firebase with minimal config for Realtime DB\n // The database URL is derived from the API response or configured\n const databaseURL = await this.getDatabaseUrl();\n\n if (!databaseURL) {\n this.log('Realtime DB URL not available, skipping real-time updates');\n\n return;\n }\n\n this.firebaseApp = initializeApp(\n {\n databaseURL,\n },\n `rolloutly-${Date.now()}`,\n );\n\n this.database = getDatabase(this.firebaseApp);\n\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n\n this.realtimeUnsubscribe = onValue(\n flagsRef,\n (snapshot) => {\n const data = snapshot.val() as Record<string, Flag> | null;\n\n if (data) {\n // Convert realtime format to our flag format\n this.flags = Object.entries(data).reduce<FlagMap>(\n (acc, [key, flag]) => {\n acc[key] = { ...flag, key };\n\n return acc;\n },\n {},\n );\n this.cacheFlags();\n this.notifyListeners();\n this.log('Realtime update received');\n }\n },\n (error) => {\n this.log('Realtime error:', error.message);\n },\n );\n }\n\n private async getDatabaseUrl(): Promise<string | null> {\n // Try to get the database URL from the API\n try {\n const url = `${this.config.baseUrl}/api/sdk/config`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (response.ok) {\n const data = (await response.json()) as { databaseUrl?: string };\n\n return data.databaseUrl ?? null;\n }\n } catch {\n // Ignore - we'll try without realtime\n }\n\n return null;\n }\n\n private loadCachedFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const cached = localStorage.getItem(CACHE_KEY);\n\n if (cached) {\n this.flags = JSON.parse(cached) as FlagMap;\n this.log('Loaded cached flags');\n }\n } catch {\n // Ignore cache errors\n }\n }\n\n private cacheFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));\n } catch {\n // Ignore cache errors\n }\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => listener());\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Rolloutly]', ...args);\n }\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Possible flag value types
|
|
3
|
+
*/
|
|
4
|
+
type FlagValue = boolean | string | number | Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* Flag data structure from the API/Realtime DB
|
|
7
|
+
*/
|
|
8
|
+
type Flag = {
|
|
9
|
+
key: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
value: FlagValue;
|
|
12
|
+
type: 'boolean' | 'string' | 'number' | 'json';
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Map of flag keys to their data
|
|
16
|
+
*/
|
|
17
|
+
type FlagMap = Record<string, Flag>;
|
|
18
|
+
/**
|
|
19
|
+
* Configuration options for RolloutlyClient
|
|
20
|
+
*/
|
|
21
|
+
type RolloutlyConfig = {
|
|
22
|
+
/** SDK token (format: rly_projectId_environmentKey_xxx) */
|
|
23
|
+
token: string;
|
|
24
|
+
/** Base URL for the API (default: https://rolloutly.com) */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
/** Enable real-time updates via Firebase (default: true) */
|
|
27
|
+
realtimeEnabled?: boolean;
|
|
28
|
+
/** Default flag values to use before flags are loaded */
|
|
29
|
+
defaultFlags?: Record<string, FlagValue>;
|
|
30
|
+
/** Enable debug logging (default: false) */
|
|
31
|
+
debug?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Client status
|
|
35
|
+
*/
|
|
36
|
+
type ClientStatus = 'initializing' | 'ready' | 'error';
|
|
37
|
+
/**
|
|
38
|
+
* Listener callback for flag changes
|
|
39
|
+
*/
|
|
40
|
+
type FlagChangeListener = () => void;
|
|
41
|
+
|
|
42
|
+
declare class RolloutlyClient {
|
|
43
|
+
private config;
|
|
44
|
+
private flags;
|
|
45
|
+
private status;
|
|
46
|
+
private error;
|
|
47
|
+
private listeners;
|
|
48
|
+
private parsedToken;
|
|
49
|
+
private firebaseApp;
|
|
50
|
+
private database;
|
|
51
|
+
private realtimeUnsubscribe;
|
|
52
|
+
private initPromise;
|
|
53
|
+
private initResolve;
|
|
54
|
+
private initReject;
|
|
55
|
+
constructor(config: RolloutlyConfig);
|
|
56
|
+
/**
|
|
57
|
+
* Wait for the client to be initialized
|
|
58
|
+
*/
|
|
59
|
+
waitForInit(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Get a single flag value
|
|
62
|
+
*/
|
|
63
|
+
getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Get all flags
|
|
66
|
+
*/
|
|
67
|
+
getFlags(): FlagMap;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a boolean flag is enabled
|
|
70
|
+
*/
|
|
71
|
+
isEnabled(key: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Get current client status
|
|
74
|
+
*/
|
|
75
|
+
getStatus(): ClientStatus;
|
|
76
|
+
/**
|
|
77
|
+
* Get the last error if any
|
|
78
|
+
*/
|
|
79
|
+
getError(): Error | null;
|
|
80
|
+
/**
|
|
81
|
+
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
82
|
+
*/
|
|
83
|
+
subscribe(listener: FlagChangeListener): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Cleanup and disconnect
|
|
86
|
+
*/
|
|
87
|
+
close(): void;
|
|
88
|
+
private parseToken;
|
|
89
|
+
private initialize;
|
|
90
|
+
private fetchFlags;
|
|
91
|
+
private setupRealtime;
|
|
92
|
+
private getDatabaseUrl;
|
|
93
|
+
private loadCachedFlags;
|
|
94
|
+
private cacheFlags;
|
|
95
|
+
private notifyListeners;
|
|
96
|
+
private log;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { type ClientStatus, type Flag, type FlagChangeListener, type FlagMap, type FlagValue, RolloutlyClient, type RolloutlyConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Possible flag value types
|
|
3
|
+
*/
|
|
4
|
+
type FlagValue = boolean | string | number | Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* Flag data structure from the API/Realtime DB
|
|
7
|
+
*/
|
|
8
|
+
type Flag = {
|
|
9
|
+
key: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
value: FlagValue;
|
|
12
|
+
type: 'boolean' | 'string' | 'number' | 'json';
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Map of flag keys to their data
|
|
16
|
+
*/
|
|
17
|
+
type FlagMap = Record<string, Flag>;
|
|
18
|
+
/**
|
|
19
|
+
* Configuration options for RolloutlyClient
|
|
20
|
+
*/
|
|
21
|
+
type RolloutlyConfig = {
|
|
22
|
+
/** SDK token (format: rly_projectId_environmentKey_xxx) */
|
|
23
|
+
token: string;
|
|
24
|
+
/** Base URL for the API (default: https://rolloutly.com) */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
/** Enable real-time updates via Firebase (default: true) */
|
|
27
|
+
realtimeEnabled?: boolean;
|
|
28
|
+
/** Default flag values to use before flags are loaded */
|
|
29
|
+
defaultFlags?: Record<string, FlagValue>;
|
|
30
|
+
/** Enable debug logging (default: false) */
|
|
31
|
+
debug?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Client status
|
|
35
|
+
*/
|
|
36
|
+
type ClientStatus = 'initializing' | 'ready' | 'error';
|
|
37
|
+
/**
|
|
38
|
+
* Listener callback for flag changes
|
|
39
|
+
*/
|
|
40
|
+
type FlagChangeListener = () => void;
|
|
41
|
+
|
|
42
|
+
declare class RolloutlyClient {
|
|
43
|
+
private config;
|
|
44
|
+
private flags;
|
|
45
|
+
private status;
|
|
46
|
+
private error;
|
|
47
|
+
private listeners;
|
|
48
|
+
private parsedToken;
|
|
49
|
+
private firebaseApp;
|
|
50
|
+
private database;
|
|
51
|
+
private realtimeUnsubscribe;
|
|
52
|
+
private initPromise;
|
|
53
|
+
private initResolve;
|
|
54
|
+
private initReject;
|
|
55
|
+
constructor(config: RolloutlyConfig);
|
|
56
|
+
/**
|
|
57
|
+
* Wait for the client to be initialized
|
|
58
|
+
*/
|
|
59
|
+
waitForInit(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Get a single flag value
|
|
62
|
+
*/
|
|
63
|
+
getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Get all flags
|
|
66
|
+
*/
|
|
67
|
+
getFlags(): FlagMap;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a boolean flag is enabled
|
|
70
|
+
*/
|
|
71
|
+
isEnabled(key: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Get current client status
|
|
74
|
+
*/
|
|
75
|
+
getStatus(): ClientStatus;
|
|
76
|
+
/**
|
|
77
|
+
* Get the last error if any
|
|
78
|
+
*/
|
|
79
|
+
getError(): Error | null;
|
|
80
|
+
/**
|
|
81
|
+
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
82
|
+
*/
|
|
83
|
+
subscribe(listener: FlagChangeListener): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Cleanup and disconnect
|
|
86
|
+
*/
|
|
87
|
+
close(): void;
|
|
88
|
+
private parseToken;
|
|
89
|
+
private initialize;
|
|
90
|
+
private fetchFlags;
|
|
91
|
+
private setupRealtime;
|
|
92
|
+
private getDatabaseUrl;
|
|
93
|
+
private loadCachedFlags;
|
|
94
|
+
private cacheFlags;
|
|
95
|
+
private notifyListeners;
|
|
96
|
+
private log;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { type ClientStatus, type Flag, type FlagChangeListener, type FlagMap, type FlagValue, RolloutlyClient, type RolloutlyConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { initializeApp } from 'firebase/app';
|
|
2
|
+
import { ref, off, getDatabase, onValue } from 'firebase/database';
|
|
3
|
+
|
|
4
|
+
// src/client.ts
|
|
5
|
+
var DEFAULT_BASE_URL = "https://rolloutly.com";
|
|
6
|
+
var CACHE_KEY = "rolloutly_flags";
|
|
7
|
+
var RolloutlyClient = class {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.flags = {};
|
|
10
|
+
this.status = "initializing";
|
|
11
|
+
this.error = null;
|
|
12
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
13
|
+
this.firebaseApp = null;
|
|
14
|
+
this.database = null;
|
|
15
|
+
this.realtimeUnsubscribe = null;
|
|
16
|
+
this.config = {
|
|
17
|
+
token: config.token,
|
|
18
|
+
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
19
|
+
realtimeEnabled: config.realtimeEnabled ?? true,
|
|
20
|
+
defaultFlags: config.defaultFlags ?? {},
|
|
21
|
+
debug: config.debug ?? false
|
|
22
|
+
};
|
|
23
|
+
const parsed = this.parseToken(config.token);
|
|
24
|
+
if (!parsed) {
|
|
25
|
+
throw new Error("Invalid SDK token format");
|
|
26
|
+
}
|
|
27
|
+
this.parsedToken = parsed;
|
|
28
|
+
this.initPromise = new Promise((resolve, reject) => {
|
|
29
|
+
this.initResolve = resolve;
|
|
30
|
+
this.initReject = reject;
|
|
31
|
+
});
|
|
32
|
+
this.loadCachedFlags();
|
|
33
|
+
void this.initialize();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wait for the client to be initialized
|
|
37
|
+
*/
|
|
38
|
+
async waitForInit() {
|
|
39
|
+
return this.initPromise;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get a single flag value
|
|
43
|
+
*/
|
|
44
|
+
getFlag(key) {
|
|
45
|
+
const flag = this.flags[key];
|
|
46
|
+
if (flag) {
|
|
47
|
+
return flag.enabled ? flag.value : this.config.defaultFlags[key];
|
|
48
|
+
}
|
|
49
|
+
return this.config.defaultFlags[key];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get all flags
|
|
53
|
+
*/
|
|
54
|
+
getFlags() {
|
|
55
|
+
return { ...this.flags };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a boolean flag is enabled
|
|
59
|
+
*/
|
|
60
|
+
isEnabled(key) {
|
|
61
|
+
const flag = this.flags[key];
|
|
62
|
+
if (!flag) {
|
|
63
|
+
return Boolean(this.config.defaultFlags[key]);
|
|
64
|
+
}
|
|
65
|
+
if (!flag.enabled) {
|
|
66
|
+
return Boolean(this.config.defaultFlags[key]);
|
|
67
|
+
}
|
|
68
|
+
return flag.type === "boolean" ? Boolean(flag.value) : true;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get current client status
|
|
72
|
+
*/
|
|
73
|
+
getStatus() {
|
|
74
|
+
return this.status;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the last error if any
|
|
78
|
+
*/
|
|
79
|
+
getError() {
|
|
80
|
+
return this.error;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe to flag changes (for React useSyncExternalStore)
|
|
84
|
+
*/
|
|
85
|
+
subscribe(listener) {
|
|
86
|
+
this.listeners.add(listener);
|
|
87
|
+
return () => {
|
|
88
|
+
this.listeners.delete(listener);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Cleanup and disconnect
|
|
93
|
+
*/
|
|
94
|
+
close() {
|
|
95
|
+
if (this.realtimeUnsubscribe) {
|
|
96
|
+
this.realtimeUnsubscribe();
|
|
97
|
+
this.realtimeUnsubscribe = null;
|
|
98
|
+
}
|
|
99
|
+
if (this.database) {
|
|
100
|
+
const flagsRef = ref(
|
|
101
|
+
this.database,
|
|
102
|
+
`flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`
|
|
103
|
+
);
|
|
104
|
+
off(flagsRef);
|
|
105
|
+
}
|
|
106
|
+
this.listeners.clear();
|
|
107
|
+
this.log("Client closed");
|
|
108
|
+
}
|
|
109
|
+
// ==================== Private Methods ====================
|
|
110
|
+
parseToken(token) {
|
|
111
|
+
const parts = token.split("_");
|
|
112
|
+
if (parts.length < 4 || parts[0] !== "rly") {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
projectId: parts[1],
|
|
117
|
+
environmentKey: parts[2]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async initialize() {
|
|
121
|
+
try {
|
|
122
|
+
await this.fetchFlags();
|
|
123
|
+
if (this.config.realtimeEnabled) {
|
|
124
|
+
await this.setupRealtime();
|
|
125
|
+
}
|
|
126
|
+
this.status = "ready";
|
|
127
|
+
this.initResolve();
|
|
128
|
+
this.log("Client initialized");
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.status = "error";
|
|
131
|
+
this.error = err instanceof Error ? err : new Error(String(err));
|
|
132
|
+
this.initReject(this.error);
|
|
133
|
+
this.log("Initialization failed:", this.error.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async fetchFlags() {
|
|
137
|
+
const url = `${this.config.baseUrl}/api/sdk/flags`;
|
|
138
|
+
const response = await fetch(url, {
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Bearer ${this.config.token}`
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`Failed to fetch flags: ${response.status}`);
|
|
145
|
+
}
|
|
146
|
+
const data = await response.json();
|
|
147
|
+
this.flags = data.flags;
|
|
148
|
+
this.cacheFlags();
|
|
149
|
+
this.notifyListeners();
|
|
150
|
+
this.log("Flags fetched:", Object.keys(this.flags).length);
|
|
151
|
+
}
|
|
152
|
+
async setupRealtime() {
|
|
153
|
+
const databaseURL = await this.getDatabaseUrl();
|
|
154
|
+
if (!databaseURL) {
|
|
155
|
+
this.log("Realtime DB URL not available, skipping real-time updates");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.firebaseApp = initializeApp(
|
|
159
|
+
{
|
|
160
|
+
databaseURL
|
|
161
|
+
},
|
|
162
|
+
`rolloutly-${Date.now()}`
|
|
163
|
+
);
|
|
164
|
+
this.database = getDatabase(this.firebaseApp);
|
|
165
|
+
const flagsRef = ref(
|
|
166
|
+
this.database,
|
|
167
|
+
`flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`
|
|
168
|
+
);
|
|
169
|
+
this.realtimeUnsubscribe = onValue(
|
|
170
|
+
flagsRef,
|
|
171
|
+
(snapshot) => {
|
|
172
|
+
const data = snapshot.val();
|
|
173
|
+
if (data) {
|
|
174
|
+
this.flags = Object.entries(data).reduce(
|
|
175
|
+
(acc, [key, flag]) => {
|
|
176
|
+
acc[key] = { ...flag, key };
|
|
177
|
+
return acc;
|
|
178
|
+
},
|
|
179
|
+
{}
|
|
180
|
+
);
|
|
181
|
+
this.cacheFlags();
|
|
182
|
+
this.notifyListeners();
|
|
183
|
+
this.log("Realtime update received");
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
(error) => {
|
|
187
|
+
this.log("Realtime error:", error.message);
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
async getDatabaseUrl() {
|
|
192
|
+
try {
|
|
193
|
+
const url = `${this.config.baseUrl}/api/sdk/config`;
|
|
194
|
+
const response = await fetch(url, {
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${this.config.token}`
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
if (response.ok) {
|
|
200
|
+
const data = await response.json();
|
|
201
|
+
return data.databaseUrl ?? null;
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
loadCachedFlags() {
|
|
208
|
+
if (typeof window === "undefined") return;
|
|
209
|
+
try {
|
|
210
|
+
const cached = localStorage.getItem(CACHE_KEY);
|
|
211
|
+
if (cached) {
|
|
212
|
+
this.flags = JSON.parse(cached);
|
|
213
|
+
this.log("Loaded cached flags");
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
cacheFlags() {
|
|
219
|
+
if (typeof window === "undefined") return;
|
|
220
|
+
try {
|
|
221
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
notifyListeners() {
|
|
226
|
+
this.listeners.forEach((listener) => listener());
|
|
227
|
+
}
|
|
228
|
+
log(...args) {
|
|
229
|
+
if (this.config.debug) {
|
|
230
|
+
console.log("[Rolloutly]", ...args);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export { RolloutlyClient };
|
|
236
|
+
//# sourceMappingURL=index.js.map
|
|
237
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 ref,\n onValue,\n off,\n type Database,\n type Unsubscribe,\n} from 'firebase/database';\n\nimport type {\n ClientStatus,\n Flag,\n FlagChangeListener,\n FlagMap,\n FlagValue,\n ParsedToken,\n RolloutlyConfig,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://rolloutly.com';\nconst CACHE_KEY = 'rolloutly_flags';\n\nexport class RolloutlyClient {\n private config: Required<\n Omit<RolloutlyConfig, 'defaultFlags'> & {\n defaultFlags: Record<string, FlagValue>;\n }\n >;\n private flags: FlagMap = {};\n private status: ClientStatus = 'initializing';\n private error: Error | null = null;\n private listeners: Set<FlagChangeListener> = new Set();\n private parsedToken: ParsedToken;\n private firebaseApp: FirebaseApp | null = null;\n private database: Database | null = null;\n private realtimeUnsubscribe: Unsubscribe | null = null;\n private initPromise: Promise<void>;\n private initResolve!: () => void;\n private initReject!: (error: Error) => void;\n\n constructor(config: RolloutlyConfig) {\n this.config = {\n token: config.token,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n realtimeEnabled: config.realtimeEnabled ?? true,\n defaultFlags: config.defaultFlags ?? {},\n debug: config.debug ?? false,\n };\n\n // Parse the token\n const parsed = this.parseToken(config.token);\n\n if (!parsed) {\n throw new Error('Invalid SDK token format');\n }\n\n this.parsedToken = parsed;\n\n // Create init promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve;\n this.initReject = reject;\n });\n\n // Load cached flags first\n this.loadCachedFlags();\n\n // Start initialization\n void this.initialize();\n }\n\n /**\n * Wait for the client to be initialized\n */\n async waitForInit(): Promise<void> {\n return this.initPromise;\n }\n\n /**\n * Get a single flag value\n */\n getFlag<T extends FlagValue = FlagValue>(key: string): T | undefined {\n const flag = this.flags[key];\n\n if (flag) {\n return (flag.enabled ? flag.value : this.config.defaultFlags[key]) as T;\n }\n\n return this.config.defaultFlags[key] as T;\n }\n\n /**\n * Get all flags\n */\n getFlags(): FlagMap {\n return { ...this.flags };\n }\n\n /**\n * Check if a boolean flag is enabled\n */\n isEnabled(key: string): boolean {\n const flag = this.flags[key];\n\n if (!flag) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n if (!flag.enabled) {\n return Boolean(this.config.defaultFlags[key]);\n }\n\n return flag.type === 'boolean' ? Boolean(flag.value) : true;\n }\n\n /**\n * Get current client status\n */\n getStatus(): ClientStatus {\n return this.status;\n }\n\n /**\n * Get the last error if any\n */\n getError(): Error | null {\n return this.error;\n }\n\n /**\n * Subscribe to flag changes (for React useSyncExternalStore)\n */\n subscribe(listener: FlagChangeListener): () => void {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Cleanup and disconnect\n */\n close(): void {\n if (this.realtimeUnsubscribe) {\n this.realtimeUnsubscribe();\n this.realtimeUnsubscribe = null;\n }\n\n if (this.database) {\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n off(flagsRef);\n }\n\n this.listeners.clear();\n this.log('Client closed');\n }\n\n // ==================== Private Methods ====================\n\n private parseToken(token: string): ParsedToken | null {\n const parts = token.split('_');\n\n // Format: rly_{projectId}_{environmentKey}_{randomString}\n if (parts.length < 4 || parts[0] !== 'rly') {\n return null;\n }\n\n return {\n projectId: parts[1],\n environmentKey: parts[2],\n };\n }\n\n private async initialize(): Promise<void> {\n try {\n // Fetch initial flags from API\n await this.fetchFlags();\n\n // Set up real-time updates if enabled\n if (this.config.realtimeEnabled) {\n await this.setupRealtime();\n }\n\n this.status = 'ready';\n this.initResolve();\n this.log('Client initialized');\n } catch (err) {\n this.status = 'error';\n this.error = err instanceof Error ? err : new Error(String(err));\n this.initReject(this.error);\n this.log('Initialization failed:', this.error.message);\n }\n }\n\n private async fetchFlags(): Promise<void> {\n const url = `${this.config.baseUrl}/api/sdk/flags`;\n\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch flags: ${response.status}`);\n }\n\n const data = (await response.json()) as { flags: FlagMap };\n this.flags = data.flags;\n this.cacheFlags();\n this.notifyListeners();\n this.log('Flags fetched:', Object.keys(this.flags).length);\n }\n\n private async setupRealtime(): Promise<void> {\n // Initialize Firebase with minimal config for Realtime DB\n // The database URL is derived from the API response or configured\n const databaseURL = await this.getDatabaseUrl();\n\n if (!databaseURL) {\n this.log('Realtime DB URL not available, skipping real-time updates');\n\n return;\n }\n\n this.firebaseApp = initializeApp(\n {\n databaseURL,\n },\n `rolloutly-${Date.now()}`,\n );\n\n this.database = getDatabase(this.firebaseApp);\n\n const flagsRef = ref(\n this.database,\n `flags/${this.parsedToken.projectId}/${this.parsedToken.environmentKey}`,\n );\n\n this.realtimeUnsubscribe = onValue(\n flagsRef,\n (snapshot) => {\n const data = snapshot.val() as Record<string, Flag> | null;\n\n if (data) {\n // Convert realtime format to our flag format\n this.flags = Object.entries(data).reduce<FlagMap>(\n (acc, [key, flag]) => {\n acc[key] = { ...flag, key };\n\n return acc;\n },\n {},\n );\n this.cacheFlags();\n this.notifyListeners();\n this.log('Realtime update received');\n }\n },\n (error) => {\n this.log('Realtime error:', error.message);\n },\n );\n }\n\n private async getDatabaseUrl(): Promise<string | null> {\n // Try to get the database URL from the API\n try {\n const url = `${this.config.baseUrl}/api/sdk/config`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.config.token}`,\n },\n });\n\n if (response.ok) {\n const data = (await response.json()) as { databaseUrl?: string };\n\n return data.databaseUrl ?? null;\n }\n } catch {\n // Ignore - we'll try without realtime\n }\n\n return null;\n }\n\n private loadCachedFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const cached = localStorage.getItem(CACHE_KEY);\n\n if (cached) {\n this.flags = JSON.parse(cached) as FlagMap;\n this.log('Loaded cached flags');\n }\n } catch {\n // Ignore cache errors\n }\n }\n\n private cacheFlags(): void {\n if (typeof window === 'undefined') return;\n\n try {\n localStorage.setItem(CACHE_KEY, JSON.stringify(this.flags));\n } catch {\n // Ignore cache errors\n }\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => listener());\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Rolloutly]', ...args);\n }\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rolloutly/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rolloutly feature flags SDK - Core JavaScript client",
|
|
5
|
+
"author": "Kevin Beltrão",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/KevBeltrao/rolloutly-sdk.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"feature-flags",
|
|
14
|
+
"feature-toggles",
|
|
15
|
+
"rolloutly",
|
|
16
|
+
"sdk"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"require": {
|
|
29
|
+
"types": "./dist/index.d.cts",
|
|
30
|
+
"default": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"clean": "rm -rf dist"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"firebase": "^10.7.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"tsup": "^8.0.1",
|
|
49
|
+
"typescript": "^5.3.3"
|
|
50
|
+
}
|
|
51
|
+
}
|