@replanejs/sdk 0.7.1 → 0.7.3

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.
Files changed (2) hide show
  1. package/README.md +480 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,480 @@
1
+ # Replane JavaScript SDK
2
+
3
+ Small TypeScript client for watching configuration values from a Replane API with realtime updates and context-based override evaluation.
4
+
5
+ Part of the Replane project: [replane-dev/replane](https://github.com/replane-dev/replane).
6
+
7
+ > Status: early. Minimal surface area on purpose. Expect small breaking tweaks until 0.1.x.
8
+
9
+ ## Why it exists
10
+
11
+ You need: given a token + config name + optional context -> watch the value with realtime updates. This package does only that:
12
+
13
+ - Works in ESM and CJS (dual build)
14
+ - Zero runtime deps (uses native `fetch` — bring a polyfill if your runtime lacks it)
15
+ - Realtime updates via Server-Sent Events (SSE)
16
+ - Context-based override evaluation (feature flags, A/B testing, gradual rollouts)
17
+ - Tiny bundle footprint
18
+ - Strong TypeScript types
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @replanejs/sdk
24
+ # or
25
+ pnpm add @replanejs/sdk
26
+ # or
27
+ yarn add @replanejs/sdk
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ > **Important:** Each SDK key is tied to a specific project. The client can only access configs from the project that the SDK key belongs to. If you need configs from multiple projects, create separate SDK keys and initialize separate clients—one per project.
33
+
34
+ ```ts
35
+ import { createReplaneClient } from "@replanejs/sdk";
36
+
37
+ // Define your config types
38
+ interface Configs {
39
+ "new-onboarding": boolean;
40
+ "password-requirements": PasswordRequirements;
41
+ "billing-enabled": boolean;
42
+ }
43
+
44
+ interface PasswordRequirements {
45
+ minLength: number;
46
+ requireSymbol: boolean;
47
+ }
48
+
49
+ const replane = await createReplaneClient<Configs>({
50
+ // Each SDK key belongs to one project only
51
+ sdkKey: process.env.REPLANE_SDK_KEY!,
52
+ baseUrl: "https://replane.my-hosting.com",
53
+ });
54
+
55
+ // Get a config value (knows about latest updates via SSE)
56
+ const featureFlag = replane.get("new-onboarding"); // Typed as boolean
57
+
58
+ if (featureFlag) {
59
+ console.log("New onboarding enabled!");
60
+ }
61
+
62
+ // Typed config - no need to specify type again
63
+ const passwordReqs = replane.get("password-requirements");
64
+
65
+ // Use the value directly
66
+ const { minLength } = passwordReqs; // TypeScript knows this is PasswordRequirements
67
+
68
+ // With context for override evaluation
69
+ const enabled = replane.get("billing-enabled", {
70
+ context: {
71
+ userId: "user-123",
72
+ plan: "premium",
73
+ region: "us-east",
74
+ },
75
+ });
76
+
77
+ if (enabled) {
78
+ console.log("Billing enabled for this user!");
79
+ }
80
+
81
+ // When done, clean up resources
82
+ replane.close();
83
+ ```
84
+
85
+ ## API
86
+
87
+ ### `createReplaneClient<T>(options)`
88
+
89
+ Returns a promise resolving to an object: `{ get, subscribe, close }`.
90
+
91
+ Type parameter `T` defines the shape of your configs (a mapping of config names to their value types).
92
+
93
+ `close()` stops the configs client and cleans up resources. It is safe to call multiple times (no‑op after the first call).
94
+
95
+ #### Options
96
+
97
+ - `baseUrl` (string) – Replane origin (no trailing slash needed).
98
+ - `sdkKey` (string) – SDK key for authorization. Required. **Note:** Each SDK key is tied to a specific project and can only access configs from that project. To access configs from multiple projects, create multiple SDK keys and initialize separate client instances.
99
+ - `required` (object or array) – mark specific configs as required. If any required config is missing, the client will throw an error during initialization. Can be an object with boolean values or an array of config names. Optional.
100
+ - `fallbacks` (object) – fallback values to use if the initial request to fetch configs fails. Allows the client to start even when the API is unavailable. Optional.
101
+ - `context` (object) – default context for all config evaluations. Can be overridden per-request in `get()`. Optional.
102
+ - `fetchFn` (function) – custom fetch (e.g. `undici.fetch` or mocked fetch in tests). Optional.
103
+ - `timeoutMs` (number) – abort the request after N ms. Default: 2000.
104
+ - `retries` (number) – number of retry attempts on failures (5xx or network errors). Default: 2.
105
+ - `retryDelayMs` (number) – base delay between retries in ms (a small jitter is applied). Default: 200.
106
+ - `logger` (object) – custom logger with `debug`, `info`, `warn`, `error` methods. Default: `console`.
107
+
108
+ ### `replane.get<K>(name, options?)`
109
+
110
+ Gets the current config value. The configs client maintains an up-to-date cache that receives realtime updates via Server-Sent Events (SSE) in the background.
111
+
112
+ Parameters:
113
+
114
+ - `name` (K extends keyof T) – config name to fetch. TypeScript will enforce that this is a valid config name from your `Configs` interface.
115
+ - `options` (object) – optional configuration:
116
+ - `context` (object) – context merged with client-level context for override evaluation.
117
+
118
+ Returns the config value of type `T[K]` (synchronous). The return type is automatically inferred from your `Configs` interface.
119
+
120
+ Notes:
121
+
122
+ - The Replane client receives realtime updates via SSE in the background.
123
+ - If the config is not found, throws a `ReplaneError` with code `not_found`.
124
+ - Context-based overrides are evaluated automatically based on context.
125
+
126
+ Example:
127
+
128
+ ```ts
129
+ interface Configs {
130
+ "billing-enabled": boolean;
131
+ "max-connections": number;
132
+ }
133
+
134
+ const replane = await createReplaneClient<Configs>({
135
+ sdkKey: "your-sdk-key",
136
+ baseUrl: "https://replane.my-host.com",
137
+ });
138
+
139
+ // Get value without context - TypeScript knows this is boolean
140
+ const enabled = replane.get("billing-enabled");
141
+
142
+ // Get value with context for override evaluation
143
+ const userEnabled = replane.get("billing-enabled", {
144
+ context: { userId: "user-123", plan: "premium" },
145
+ });
146
+
147
+ // Clean up when done
148
+ replane.close();
149
+ ```
150
+
151
+ ### `replane.subscribe(callback)` or `replane.subscribe(configName, callback)`
152
+
153
+ Subscribe to config changes and receive real-time updates when configs are modified.
154
+
155
+ **Two overloads:**
156
+
157
+ 1. **Subscribe to all config changes:**
158
+
159
+ ```ts
160
+ const unsubscribe = replane.subscribe((config) => {
161
+ console.log(`Config ${config.name} changed to:`, config.value);
162
+ });
163
+ ```
164
+
165
+ 2. **Subscribe to a specific config:**
166
+ ```ts
167
+ const unsubscribe = replane.subscribe("billing-enabled", (config) => {
168
+ console.log(`billing-enabled changed to:`, config.value);
169
+ });
170
+ ```
171
+
172
+ Parameters:
173
+
174
+ - `callback` (function) – Function called when any config changes. Receives an object with `{ name, value }`.
175
+ - `configName` (K extends keyof T) – Optional. If provided, only changes to this specific config will trigger the callback.
176
+
177
+ Returns a function to unsubscribe from the config changes.
178
+
179
+ Example:
180
+
181
+ ```ts
182
+ interface Configs {
183
+ "feature-flag": boolean;
184
+ "max-connections": number;
185
+ }
186
+
187
+ const replane = await createReplaneClient<Configs>({
188
+ sdkKey: "your-sdk-key",
189
+ baseUrl: "https://replane.my-host.com",
190
+ });
191
+
192
+ // Subscribe to all config changes
193
+ const unsubscribeAll = replane.subscribe((config) => {
194
+ console.log(`Config ${config.name} updated:`, config.value);
195
+ });
196
+
197
+ // Subscribe to a specific config
198
+ const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
199
+ console.log("Feature flag changed:", config.value);
200
+ // config.value is typed as boolean
201
+ });
202
+
203
+ // Later: unsubscribe when done
204
+ unsubscribeAll();
205
+ unsubscribeFeature();
206
+
207
+ // Clean up when done
208
+ replane.close();
209
+ ```
210
+
211
+ ### `createInMemoryReplaneClient(initialData)`
212
+
213
+ Creates a client backed by an in-memory store instead of making HTTP requests. Handy for unit tests or local development where you want deterministic config values without a server.
214
+
215
+ Parameters:
216
+
217
+ - `initialData` (object) – map of config name to value.
218
+
219
+ Returns the same client shape as `createReplaneClient` (`{ get, subscribe, close }`).
220
+
221
+ Notes:
222
+
223
+ - `get(name)` resolves to the value from `initialData`.
224
+ - If a name is missing, it throws a `ReplaneError` (`Config not found: <name>`).
225
+ - The client works as usual but doesn't receive SSE updates (values remain whatever is in-memory).
226
+
227
+ Example:
228
+
229
+ ```ts
230
+ import { createInMemoryReplaneClient } from "@replanejs/sdk";
231
+
232
+ interface Configs {
233
+ "feature-a": boolean;
234
+ "max-items": { value: number; ttl: number };
235
+ }
236
+
237
+ const replane = createInMemoryReplaneClient<Configs>({
238
+ "feature-a": true,
239
+ "max-items": { value: 10, ttl: 3600 },
240
+ });
241
+
242
+ const featureA = replane.get("feature-a"); // TypeScript knows this is boolean
243
+ console.log(featureA); // true
244
+
245
+ const maxItems = replane.get("max-items"); // TypeScript knows the type
246
+ console.log(maxItems); // { value: 10, ttl: 3600 }
247
+
248
+ replane.close();
249
+ ```
250
+
251
+ ### `replane.close()`
252
+
253
+ Gracefully shuts down the Replane client and cleans up resources. Subsequent method calls will throw. Use this in environments where you manage resource lifecycles explicitly (e.g. shutting down a server or worker).
254
+
255
+ ```ts
256
+ // During shutdown
257
+ replane.close();
258
+ ```
259
+
260
+ ### Errors
261
+
262
+ `createReplaneClient` throws if the initial request to fetch configs fails with non‑2xx HTTP responses and network errors. A `ReplaneError` is thrown for HTTP failures; other errors may be thrown for network/parse issues.
263
+
264
+ The Replane client receives realtime updates via SSE in the background. SSE connection errors are logged and automatically retried, but don't affect `get` calls (which return the last known value).
265
+
266
+ ## Environment notes
267
+
268
+ - Node 18+ has global `fetch`; for older Node versions supply `fetchFn`.
269
+ - Edge runtimes / Workers: provide a compatible `fetch` + `AbortController` if not built‑in.
270
+
271
+ ## Common patterns
272
+
273
+ ### Typed config
274
+
275
+ ```ts
276
+ interface LayoutConfig {
277
+ variant: "a" | "b";
278
+ ttl: number;
279
+ }
280
+
281
+ interface Configs {
282
+ layout: LayoutConfig;
283
+ }
284
+
285
+ const replane = await createReplaneClient<Configs>({
286
+ sdkKey: process.env.REPLANE_SDK_KEY!,
287
+ baseUrl: "https://replane.my-host.com",
288
+ });
289
+
290
+ const layout = replane.get("layout"); // TypeScript knows this is LayoutConfig
291
+ console.log(layout); // { variant: "a", ttl: 3600 }
292
+ ```
293
+
294
+ ### Context-based overrides
295
+
296
+ ```ts
297
+ interface Configs {
298
+ "advanced-features": boolean;
299
+ }
300
+
301
+ const replane = await createReplaneClient<Configs>({
302
+ sdkKey: process.env.REPLANE_SDK_KEY!,
303
+ baseUrl: "https://replane.my-host.com",
304
+ });
305
+
306
+ // Config has base value `false` but override: if `plan === "premium"` then `true`
307
+
308
+ // Free user
309
+ const freeUserEnabled = replane.get("advanced-features", {
310
+ context: { plan: "free" },
311
+ }); // false
312
+
313
+ // Premium user
314
+ const premiumUserEnabled = replane.get("advanced-features", {
315
+ context: { plan: "premium" },
316
+ }); // true
317
+ ```
318
+
319
+ ### Client-level context
320
+
321
+ ```ts
322
+ interface Configs {
323
+ "feature-flag": boolean;
324
+ }
325
+
326
+ const replane = await createReplaneClient<Configs>({
327
+ sdkKey: process.env.REPLANE_SDK_KEY!,
328
+ baseUrl: "https://replane.my-host.com",
329
+ context: {
330
+ userId: "user-123",
331
+ region: "us-east",
332
+ },
333
+ });
334
+
335
+ // This context is used for all configs unless overridden
336
+ const value1 = replane.get("feature-flag"); // Uses client-level context
337
+ const value2 = replane.get("feature-flag", {
338
+ context: { userId: "user-321" },
339
+ }); // Merges with client context
340
+ ```
341
+
342
+ ### Custom fetch (tests)
343
+
344
+ ```ts
345
+ const replane = await createReplaneClient({
346
+ sdkKey: "TKN",
347
+ baseUrl: "https://api",
348
+ fetchFn: mockFetch,
349
+ });
350
+ ```
351
+
352
+ ### Required configs
353
+
354
+ ```ts
355
+ interface Configs {
356
+ "api-key": string;
357
+ "database-url": string;
358
+ "optional-feature": boolean;
359
+ }
360
+
361
+ const replane = await createReplaneClient<Configs>({
362
+ sdkKey: process.env.REPLANE_SDK_KEY!,
363
+ baseUrl: "https://replane.my-host.com",
364
+ required: {
365
+ "api-key": true,
366
+ "database-url": true,
367
+ "optional-feature": false, // Not required
368
+ },
369
+ });
370
+
371
+ // Alternative: use an array
372
+ // required: ["api-key", "database-url"]
373
+
374
+ // If any required config is missing, initialization will throw
375
+ ```
376
+
377
+ ### Fallback configs
378
+
379
+ ```ts
380
+ interface Configs {
381
+ "feature-flag": boolean;
382
+ "max-connections": number;
383
+ "timeout-ms": number;
384
+ }
385
+
386
+ const replane = await createReplaneClient<Configs>({
387
+ sdkKey: process.env.REPLANE_SDK_KEY!,
388
+ baseUrl: "https://replane.my-host.com",
389
+ fallbacks: {
390
+ "feature-flag": false, // Use false if fetch fails
391
+ "max-connections": 10, // Use 10 if fetch fails
392
+ "timeout-ms": 5000, // Use 5s if fetch fails
393
+ },
394
+ });
395
+
396
+ // If the initial fetch fails, fallback values are used
397
+ // Once the configs client connects, it will receive realtime updates
398
+ const maxConnections = replane.get("max-connections"); // 10 (or real value)
399
+ ```
400
+
401
+ ### Multiple projects
402
+
403
+ ```ts
404
+ interface ProjectAConfigs {
405
+ "feature-flag": boolean;
406
+ "max-users": number;
407
+ }
408
+
409
+ interface ProjectBConfigs {
410
+ "feature-flag": boolean;
411
+ "api-rate-limit": number;
412
+ }
413
+
414
+ // Each project needs its own SDK key and Replane client instance
415
+ const projectAConfigs = await createReplaneClient<ProjectAConfigs>({
416
+ sdkKey: process.env.PROJECT_A_SDK_KEY!,
417
+ baseUrl: "https://replane.my-host.com",
418
+ });
419
+
420
+ const projectBConfigs = await createReplaneClient<ProjectBConfigs>({
421
+ sdkKey: process.env.PROJECT_B_SDK_KEY!,
422
+ baseUrl: "https://replane.my-host.com",
423
+ });
424
+
425
+ // Each Replane client only accesses configs from its respective project
426
+ const featureA = projectAConfigs.get("feature-flag"); // boolean
427
+ const featureB = projectBConfigs.get("feature-flag"); // boolean
428
+ ```
429
+
430
+ ### Subscriptions
431
+
432
+ ```ts
433
+ interface Configs {
434
+ "feature-flag": boolean;
435
+ "max-users": number;
436
+ }
437
+
438
+ const replane = await createReplaneClient<Configs>({
439
+ sdkKey: process.env.REPLANE_SDK_KEY!,
440
+ baseUrl: "https://replane.my-host.com",
441
+ });
442
+
443
+ // Subscribe to all config changes
444
+ const unsubscribeAll = replane.subscribe((config) => {
445
+ console.log(`Config ${config.name} changed:`, config.value);
446
+
447
+ // React to specific config changes
448
+ if (config.name === "feature-flag") {
449
+ console.log("Feature flag updated:", config.value);
450
+ }
451
+ });
452
+
453
+ // Subscribe to a specific config only
454
+ const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
455
+ console.log("Feature flag changed:", config.value);
456
+ // config.value is automatically typed as boolean
457
+ });
458
+
459
+ // Subscribe to multiple specific configs
460
+ const unsubscribeMaxUsers = replane.subscribe("max-users", (config) => {
461
+ console.log("Max users changed:", config.value);
462
+ // config.value is automatically typed as number
463
+ });
464
+
465
+ // Cleanup
466
+ unsubscribeAll();
467
+ unsubscribeFeature();
468
+ unsubscribeMaxUsers();
469
+ replane.close();
470
+ ```
471
+
472
+ ## Roadmap
473
+
474
+ - Config caching
475
+ - Config invalidation
476
+
477
+ ## License
478
+
479
+ MIT
480
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replanejs/sdk",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Dynamic configuration SDK for browser and server environments (Node.js, Deno, Bun). Powered by Replane.",
5
5
  "type": "module",
6
6
  "license": "MIT",