anmo 1.0.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,243 @@
1
+ # Anmo SDK
2
+
3
+ Official Node.js SDK for Anmo Feature Flags - a modern, real-time feature flag management service.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install anmo
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { AnmoClient } from "anmo";
15
+
16
+ const client = new AnmoClient({
17
+ apiKey: "anmo_your_api_key_here",
18
+ autoFetch: true, // Automatically fetch flags on init
19
+ autoStream: true, // Automatically start streaming updates
20
+ });
21
+
22
+ // Wait for initial flags to load
23
+ await client.ready();
24
+
25
+ // Check if a flag is enabled
26
+ if (client.isEnabled("new-dashboard")) {
27
+ console.log("New dashboard is enabled!");
28
+ }
29
+ ```
30
+
31
+ ## Features
32
+
33
+ - 🚀 **Real-time Updates** - Automatically sync flag changes via Server-Sent Events
34
+ - 🔒 **Type-safe** - Full TypeScript support with type definitions
35
+ - 📦 **Lightweight** - Minimal dependencies
36
+ - 🎯 **Simple API** - Easy to use, hard to misuse
37
+ - ⚡ **Fast** - Built for performance
38
+ - ⚛️ **React Hooks** - First-class React support with custom hooks
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install anmo
44
+ ```
45
+
46
+ For React applications, React 16.8+ is required (for hooks support).
47
+
48
+ ## Usage
49
+
50
+ ### Basic Example
51
+
52
+ ```typescript
53
+ import { AnmoClient } from "anmo";
54
+
55
+ const client = new AnmoClient({
56
+ apiKey: "anmo_your_api_key_here",
57
+ baseUrl: "https://your-anmo-instance.com",
58
+ });
59
+
60
+ // Wait for initial flags to load
61
+ await client.ready();
62
+
63
+ // Check individual flag
64
+ if (client.isEnabled("new-dashboard")) {
65
+ console.log("New dashboard is enabled!");
66
+ }
67
+
68
+ // Get a specific flag with a default value
69
+ const darkMode = client.getFlag("dark-mode", false);
70
+
71
+ // Get all flags
72
+ const allFlags = client.getAllFlags();
73
+ console.log("All flags:", allFlags);
74
+ ```
75
+
76
+ ### Real-time Updates
77
+
78
+ ```typescript
79
+ const client = new AnmoClient({
80
+ apiKey: "anmo_your_api_key_here",
81
+ baseUrl: "https://your-anmo-instance.com",
82
+ autoStream: true, // Start streaming immediately
83
+ });
84
+
85
+ // Listen for flag updates
86
+ client.onUpdate((flags) => {
87
+ console.log("Flags updated:", flags);
88
+
89
+ // React to changes
90
+ if (flags["maintenance-mode"]) {
91
+ showMaintenanceBanner();
92
+ }
93
+ });
94
+
95
+ // Clean up when done
96
+ process.on("SIGINT", () => {
97
+ client.destroy();
98
+ process.exit();
99
+ });
100
+ ```
101
+
102
+ ### Manual Streaming Control
103
+
104
+ ```typescript
105
+ const client = new AnmoClient({
106
+ apiKey: "anmo_your_api_key_here",
107
+ baseUrl: "https://your-anmo-instance.com",
108
+ autoStream: false, // Don't auto-start
109
+ });
110
+
111
+ // Start streaming manually
112
+ client.startStream();
113
+
114
+ // Stop streaming when needed
115
+ client.stopStream();
116
+ ```
117
+
118
+ ### React Usage
119
+
120
+ The SDK provides React hooks for easy integration:
121
+
122
+ #### Basic Hook
123
+
124
+ ```tsx
125
+ import { useFeatureFlag } from "anmo/react";
126
+
127
+ function App() {
128
+ const showNewDashboard = useFeatureFlag("new-dashboard", {
129
+ apiKey: process.env.REACT_APP_ANMO_API_KEY!,
130
+ });
131
+ const useDarkMode = useFeatureFlag("use-dark-mode", {
132
+ apiKey: process.env.REACT_APP_ANMO_API_KEY!,
133
+ });
134
+
135
+ if (loading) return <div>Loading...</div>;
136
+ if (error) return <div>Error: {error.message}</div>;
137
+
138
+ return (
139
+ <div>
140
+ {showNewDashboard && <NewDashboard />}
141
+ {useDarkMode && <DarkModeToggle />}
142
+ </div>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ## React API Reference
148
+
149
+ ### `useFeatureFlag(flagKey, options, defaultValue?)`
150
+
151
+ Hook for a single feature flag. Returns boolean indicating if the flag is enabled.
152
+
153
+ ## Node.js API Reference
154
+
155
+ ### Constructor Options
156
+
157
+ ```typescript
158
+ interface AnmoClientOptions {
159
+ apiKey: string; // Required: Your Anmo API key
160
+ baseUrl?: string; // Optional: Base URL (default: 'http://localhost:3000')
161
+ autoFetch?: boolean; // Optional: Auto-fetch on init (default: true)
162
+ autoStream?: boolean; // Optional: Auto-start streaming (default: false)
163
+ }
164
+ ```
165
+
166
+ ### Methods
167
+
168
+ #### `ready(): Promise<void>`
169
+
170
+ Wait for the client to be ready (initial flags fetched).
171
+
172
+ #### `getFlags(): Promise<FlagMap>`
173
+
174
+ Manually fetch all feature flags. Usually not needed if `autoFetch` is enabled.
175
+
176
+ #### `isEnabled(key: string, defaultValue?: boolean): boolean`
177
+
178
+ Check if a feature flag is enabled. Returns `defaultValue` (default: `false`) if flag doesn't exist.
179
+
180
+ #### `getFlag(key: string, defaultValue?: boolean): boolean`
181
+
182
+ Alias for `isEnabled()`.
183
+
184
+ #### `getAllFlags(): FlagMap`
185
+
186
+ Get a copy of all current flags as an object.
187
+
188
+ #### `onUpdate(listener: (flags: FlagMap) => void): () => void`
189
+
190
+ Subscribe to flag updates. Returns an unsubscribe function.
191
+
192
+ ```typescript
193
+ const unsubscribe = client.onUpdate((flags) => {
194
+ console.log("Updated flags:", flags);
195
+ });
196
+
197
+ // Later, to unsubscribe:
198
+ unsubscribe();
199
+ ```
200
+
201
+ #### `startStream(): void`
202
+
203
+ Start streaming real-time updates via Server-Sent Events.
204
+
205
+ #### `stopStream(): void`
206
+
207
+ Stop streaming updates and close the connection.
208
+
209
+ #### `destroy(): void`
210
+
211
+ Clean up all resources, close connections, and remove all listeners.
212
+
213
+ ## Publishing to npm
214
+
215
+ To publish this package to npm:
216
+
217
+ ```bash
218
+ cd sdks/javascript
219
+ npm install
220
+ npm run build
221
+ npm publish --access public
222
+ ```
223
+
224
+ ## Development
225
+
226
+ To work on the SDK locally:
227
+
228
+ ```bash
229
+ cd sdks/javascript
230
+ npm install
231
+ npm run build
232
+ npm link
233
+ ```
234
+
235
+ Then in your project:
236
+
237
+ ```bash
238
+ npm link anmo
239
+ ```
240
+
241
+ ## License
242
+
243
+ MIT
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Type representing a map of feature flag keys to their enabled state
3
+ */
4
+ export type FlagMap = Record<string, boolean>;
5
+ /**
6
+ * Configuration options for the Anmo client
7
+ */
8
+ export interface AnmoClientOptions {
9
+ /**
10
+ * Your Anmo API key (starts with 'anmo_')
11
+ */
12
+ apiKey: string;
13
+ /**
14
+ * Base URL of your Anmo instance
15
+ * @default 'https://anmo.io'
16
+ */
17
+ baseUrl?: string;
18
+ /**
19
+ * Whether to automatically fetch flags on initialization
20
+ * @default true
21
+ */
22
+ autoFetch?: boolean;
23
+ /**
24
+ * Whether to automatically start streaming updates
25
+ * @default false
26
+ */
27
+ autoStream?: boolean;
28
+ }
29
+ /**
30
+ * Anmo Feature Flag Client
31
+ *
32
+ * This client provides methods to interact with the Anmo feature flag service,
33
+ * including fetching flags, checking if flags are enabled, and subscribing to
34
+ * real-time updates via Server-Sent Events (SSE).
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const client = new AnmoClient({
39
+ * apiKey: 'anmo_your_api_key_here',
40
+ * baseUrl: 'https://your-anmo-instance.com',
41
+ * autoFetch: true,
42
+ * autoStream: true
43
+ * });
44
+ *
45
+ * // Wait for initial flags to load
46
+ * await client.ready();
47
+ *
48
+ * // Check if a flag is enabled
49
+ * if (client.isEnabled('new-dashboard')) {
50
+ * console.log('New dashboard is enabled!');
51
+ * }
52
+ *
53
+ * // Listen for flag updates
54
+ * client.onUpdate((flags) => {
55
+ * console.log('Flags updated:', flags);
56
+ * });
57
+ * ```
58
+ */
59
+ export declare class AnmoClient {
60
+ private apiKey;
61
+ private baseUrl;
62
+ private flags;
63
+ private eventSource;
64
+ private updateListeners;
65
+ private readyPromise;
66
+ private readyResolve;
67
+ private isReady;
68
+ constructor(options: AnmoClientOptions);
69
+ /**
70
+ * Wait for the client to be ready (initial flags fetched)
71
+ * @returns Promise that resolves when flags are ready
72
+ */
73
+ ready(): Promise<void>;
74
+ /**
75
+ * Fetch all feature flags for the environment
76
+ * @returns Promise resolving to the flag map
77
+ */
78
+ getFlags(): Promise<FlagMap>;
79
+ /**
80
+ * Check if a feature flag is enabled
81
+ * @param key - The flag key to check
82
+ * @param defaultValue - Default value if flag is not found (default: false)
83
+ * @returns Whether the flag is enabled
84
+ */
85
+ isEnabled(key: string, defaultValue?: boolean): boolean;
86
+ /**
87
+ * Get the value of a specific flag
88
+ * @param key - The flag key
89
+ * @param defaultValue - Default value if flag is not found
90
+ * @returns The flag value
91
+ */
92
+ getFlag(key: string, defaultValue?: boolean): boolean;
93
+ /**
94
+ * Get all current flags
95
+ * @returns A copy of all flags
96
+ */
97
+ getAllFlags(): FlagMap;
98
+ /**
99
+ * Subscribe to flag updates
100
+ * @param listener - Callback function to call when flags update
101
+ * @returns Unsubscribe function
102
+ */
103
+ onUpdate(listener: (flags: FlagMap) => void): () => void;
104
+ /**
105
+ * Start streaming real-time updates via Server-Sent Events
106
+ */
107
+ startStream(): void;
108
+ /**
109
+ * Stop streaming updates
110
+ */
111
+ stopStream(): void;
112
+ /**
113
+ * Notify all listeners of flag updates
114
+ */
115
+ private notifyListeners;
116
+ /**
117
+ * Clean up resources and close connections
118
+ */
119
+ destroy(): void;
120
+ }
121
+ export default AnmoClient;
package/dist/index.js ADDED
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AnmoClient = void 0;
7
+ const eventsource_1 = __importDefault(require("eventsource"));
8
+ /**
9
+ * Anmo Feature Flag Client
10
+ *
11
+ * This client provides methods to interact with the Anmo feature flag service,
12
+ * including fetching flags, checking if flags are enabled, and subscribing to
13
+ * real-time updates via Server-Sent Events (SSE).
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const client = new AnmoClient({
18
+ * apiKey: 'anmo_your_api_key_here',
19
+ * baseUrl: 'https://your-anmo-instance.com',
20
+ * autoFetch: true,
21
+ * autoStream: true
22
+ * });
23
+ *
24
+ * // Wait for initial flags to load
25
+ * await client.ready();
26
+ *
27
+ * // Check if a flag is enabled
28
+ * if (client.isEnabled('new-dashboard')) {
29
+ * console.log('New dashboard is enabled!');
30
+ * }
31
+ *
32
+ * // Listen for flag updates
33
+ * client.onUpdate((flags) => {
34
+ * console.log('Flags updated:', flags);
35
+ * });
36
+ * ```
37
+ */
38
+ class AnmoClient {
39
+ constructor(options) {
40
+ this.flags = {};
41
+ this.eventSource = null;
42
+ this.updateListeners = [];
43
+ this.readyPromise = null;
44
+ this.readyResolve = null;
45
+ this.isReady = false;
46
+ if (!options.apiKey) {
47
+ throw new Error("API key is required");
48
+ }
49
+ if (!options.apiKey.startsWith("anmo_")) {
50
+ console.warn('API key should start with "anmo_"');
51
+ }
52
+ this.apiKey = options.apiKey;
53
+ this.baseUrl = options.baseUrl || "https://anmo.io";
54
+ // Create ready promise
55
+ this.readyPromise = new Promise((resolve) => {
56
+ this.readyResolve = resolve;
57
+ });
58
+ // Auto-fetch flags if enabled
59
+ if (options.autoFetch !== false) {
60
+ this.getFlags().catch((error) => {
61
+ console.error("Failed to fetch initial flags:", error);
62
+ });
63
+ }
64
+ // Auto-start streaming if enabled
65
+ if (options.autoStream === true) {
66
+ this.startStream();
67
+ }
68
+ }
69
+ /**
70
+ * Wait for the client to be ready (initial flags fetched)
71
+ * @returns Promise that resolves when flags are ready
72
+ */
73
+ async ready() {
74
+ if (this.isReady)
75
+ return;
76
+ return this.readyPromise;
77
+ }
78
+ /**
79
+ * Fetch all feature flags for the environment
80
+ * @returns Promise resolving to the flag map
81
+ */
82
+ async getFlags() {
83
+ try {
84
+ const response = await fetch(`${this.baseUrl}/api/client/flags`, {
85
+ headers: {
86
+ "x-api-key": this.apiKey,
87
+ },
88
+ });
89
+ if (!response.ok) {
90
+ const error = (await response
91
+ .json()
92
+ .catch(() => ({ error: "Unknown error" })));
93
+ throw new Error(`Failed to fetch flags: ${response.statusText} - ${error.error || ""}`);
94
+ }
95
+ const data = (await response.json());
96
+ this.flags = data.flags;
97
+ if (!this.isReady) {
98
+ this.isReady = true;
99
+ this.readyResolve?.();
100
+ }
101
+ return this.flags;
102
+ }
103
+ catch (error) {
104
+ console.error("Error fetching flags:", error);
105
+ throw error;
106
+ }
107
+ }
108
+ /**
109
+ * Check if a feature flag is enabled
110
+ * @param key - The flag key to check
111
+ * @param defaultValue - Default value if flag is not found (default: false)
112
+ * @returns Whether the flag is enabled
113
+ */
114
+ isEnabled(key, defaultValue = false) {
115
+ if (!(key in this.flags)) {
116
+ return defaultValue;
117
+ }
118
+ return this.flags[key] === true;
119
+ }
120
+ /**
121
+ * Get the value of a specific flag
122
+ * @param key - The flag key
123
+ * @param defaultValue - Default value if flag is not found
124
+ * @returns The flag value
125
+ */
126
+ getFlag(key, defaultValue = false) {
127
+ return this.isEnabled(key, defaultValue);
128
+ }
129
+ /**
130
+ * Get all current flags
131
+ * @returns A copy of all flags
132
+ */
133
+ getAllFlags() {
134
+ return { ...this.flags };
135
+ }
136
+ /**
137
+ * Subscribe to flag updates
138
+ * @param listener - Callback function to call when flags update
139
+ * @returns Unsubscribe function
140
+ */
141
+ onUpdate(listener) {
142
+ this.updateListeners.push(listener);
143
+ // Return unsubscribe function
144
+ return () => {
145
+ const index = this.updateListeners.indexOf(listener);
146
+ if (index > -1) {
147
+ this.updateListeners.splice(index, 1);
148
+ }
149
+ };
150
+ }
151
+ /**
152
+ * Start streaming real-time updates via Server-Sent Events
153
+ */
154
+ startStream() {
155
+ if (this.eventSource) {
156
+ console.warn("Stream already started");
157
+ return;
158
+ }
159
+ const url = `${this.baseUrl}/api/client/stream`;
160
+ this.eventSource = new eventsource_1.default(url, {
161
+ headers: {
162
+ "x-api-key": this.apiKey,
163
+ },
164
+ });
165
+ this.eventSource.onmessage = (event) => {
166
+ try {
167
+ const data = JSON.parse(event.data);
168
+ if (data.type === "initial" || data.type === "update") {
169
+ this.flags = data.flags;
170
+ if (!this.isReady) {
171
+ this.isReady = true;
172
+ this.readyResolve?.();
173
+ }
174
+ this.notifyListeners();
175
+ }
176
+ }
177
+ catch (error) {
178
+ console.error("Error parsing SSE message:", error);
179
+ }
180
+ };
181
+ this.eventSource.onerror = (error) => {
182
+ console.error("SSE error:", error);
183
+ this.stopStream();
184
+ // Attempt to reconnect after 5 seconds
185
+ setTimeout(() => {
186
+ console.log("Attempting to reconnect...");
187
+ this.startStream();
188
+ }, 5000);
189
+ };
190
+ }
191
+ /**
192
+ * Stop streaming updates
193
+ */
194
+ stopStream() {
195
+ if (this.eventSource) {
196
+ this.eventSource.close();
197
+ this.eventSource = null;
198
+ }
199
+ }
200
+ /**
201
+ * Notify all listeners of flag updates
202
+ */
203
+ notifyListeners() {
204
+ const currentFlags = this.getAllFlags();
205
+ this.updateListeners.forEach((listener) => {
206
+ try {
207
+ listener(currentFlags);
208
+ }
209
+ catch (error) {
210
+ console.error("Error in update listener:", error);
211
+ }
212
+ });
213
+ }
214
+ /**
215
+ * Clean up resources and close connections
216
+ */
217
+ destroy() {
218
+ this.stopStream();
219
+ this.updateListeners = [];
220
+ }
221
+ }
222
+ exports.AnmoClient = AnmoClient;
223
+ // Re-export for convenience
224
+ exports.default = AnmoClient;
@@ -0,0 +1,18 @@
1
+ import type { FlagMap, AnmoClientOptions } from "./index";
2
+ export type { FlagMap, AnmoClientOptions };
3
+ /**
4
+ * React hook for a single feature flag with real-time updates
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * function MyComponent() {
9
+ * const isNewDashboard = useFeatureFlag('new-dashboard', {
10
+ * apiKey: 'anmo_your_api_key',
11
+ * baseUrl: 'https://anmo.io'
12
+ * }, false);
13
+ *
14
+ * return isNewDashboard ? <NewDashboard /> : <OldDashboard />;
15
+ * }
16
+ * ```
17
+ */
18
+ export declare function useFeatureFlag(flagKey: string, options: AnmoClientOptions, defaultValue?: boolean): boolean;
package/dist/react.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFeatureFlag = useFeatureFlag;
4
+ const react_1 = require("react");
5
+ const index_1 = require("./index");
6
+ /**
7
+ * React hook for a single feature flag with real-time updates
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * function MyComponent() {
12
+ * const isNewDashboard = useFeatureFlag('new-dashboard', {
13
+ * apiKey: 'anmo_your_api_key',
14
+ * baseUrl: 'https://anmo.io'
15
+ * }, false);
16
+ *
17
+ * return isNewDashboard ? <NewDashboard /> : <OldDashboard />;
18
+ * }
19
+ * ```
20
+ */
21
+ function useFeatureFlag(flagKey, options, defaultValue = false) {
22
+ const [value, setValue] = (0, react_1.useState)(defaultValue);
23
+ // Create client instance (memoized by API key and URL)
24
+ const client = (0, react_1.useMemo)(() => {
25
+ return new index_1.AnmoClient({
26
+ ...options,
27
+ autoFetch: false,
28
+ autoStream: false,
29
+ });
30
+ }, [options.apiKey, options.baseUrl]);
31
+ (0, react_1.useEffect)(() => {
32
+ let mounted = true;
33
+ // Fetch initial flags
34
+ const fetchFlags = async () => {
35
+ try {
36
+ const flags = await client.getFlags();
37
+ if (mounted) {
38
+ setValue(flags[flagKey] ?? defaultValue);
39
+ }
40
+ }
41
+ catch (err) {
42
+ console.error("Failed to fetch flags:", err);
43
+ if (mounted) {
44
+ setValue(defaultValue);
45
+ }
46
+ }
47
+ };
48
+ fetchFlags();
49
+ // Subscribe to updates
50
+ const unsubscribe = client.onUpdate((updatedFlags) => {
51
+ if (mounted) {
52
+ setValue(updatedFlags[flagKey] ?? defaultValue);
53
+ }
54
+ });
55
+ // Start streaming
56
+ client.startStream();
57
+ // Cleanup
58
+ return () => {
59
+ mounted = false;
60
+ unsubscribe();
61
+ client.destroy();
62
+ };
63
+ }, [client, flagKey, defaultValue]);
64
+ return value;
65
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "anmo",
3
+ "version": "1.0.0",
4
+ "description": "Official Node.js SDK for Anmo Feature Flags",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./react": {
13
+ "types": "./dist/react.d.ts",
14
+ "default": "./dist/react.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "prepublishOnly": "npm run build",
24
+ "clean": "rm -rf dist",
25
+ "test": "npm run build && node test.js"
26
+ },
27
+ "keywords": [
28
+ "feature-flags",
29
+ "feature-toggles",
30
+ "anmo",
31
+ "sdk",
32
+ "nodejs"
33
+ ],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "eventsource": "^2.0.2"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=16.8.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "react": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@types/eventsource": "^1.1.15",
49
+ "@types/node": "^22.0.0",
50
+ "@types/react": "^18.0.0",
51
+ "typescript": "^5.0.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/AndrewVos/anmo"
59
+ }
60
+ }