@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 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"]}
@@ -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 };
@@ -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
+ }