@replanejs/sdk 0.5.6
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/LICENSE +21 -0
- package/README.md +376 -0
- package/dist/index.cjs +565 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +477 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dmitry Tilyupo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
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 replane-sdk
|
|
24
|
+
# or
|
|
25
|
+
pnpm add replane-sdk
|
|
26
|
+
# or
|
|
27
|
+
yarn add replane-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 "replane-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 client = 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 = client.getConfig("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 = client.getConfig("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 = client.getConfig("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
|
+
client.close();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
### `createReplaneClient<T>(options)`
|
|
88
|
+
|
|
89
|
+
Returns a promise resolving to an object: `{ getConfig, 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 client and cleans up resources. After calling it, any subsequent call to `getConfig` will throw. 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
|
+
- `requiredConfigs` (object) – mark specific configs as required. If any required config is missing, the client will throw an error during initialization. Optional.
|
|
100
|
+
- `fallbackConfigs` (object) – fallback values to use if the initial request to fetch configs fails. Allows the client to start even when the API is unavailable. Use explicit `undefined` for configs without fallbacks. Optional.
|
|
101
|
+
- `context` (object) – default context for all config evaluations. Can be overridden per-request in `getConfig()`. 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
|
+
### `client.getConfig<K>(name, options?)`
|
|
109
|
+
|
|
110
|
+
Gets the current config value. The 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 client receives realtime updates via SSE in the background.
|
|
123
|
+
- Values are automatically refreshed every 60 seconds as a fallback.
|
|
124
|
+
- If the config is not found, throws a `ReplaneError` with code `not_found`.
|
|
125
|
+
- Context-based overrides are evaluated automatically based on context.
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
interface Configs {
|
|
131
|
+
"billing-enabled": boolean;
|
|
132
|
+
"max-connections": number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const client = await createReplaneClient<Configs>({
|
|
136
|
+
sdkKey: "your-sdk-key",
|
|
137
|
+
baseUrl: "https://replane.my-host.com",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Get value without context - TypeScript knows this is boolean
|
|
141
|
+
const enabled = client.getConfig("billing-enabled");
|
|
142
|
+
|
|
143
|
+
// Get value with context for override evaluation
|
|
144
|
+
const userEnabled = client.getConfig("billing-enabled", {
|
|
145
|
+
context: { userId: "user-123", plan: "premium" },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Clean up when done
|
|
149
|
+
client.close();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `createInMemoryReplaneClient(initialData)`
|
|
153
|
+
|
|
154
|
+
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.
|
|
155
|
+
|
|
156
|
+
Parameters:
|
|
157
|
+
|
|
158
|
+
- `initialData` (object) – map of config name to value.
|
|
159
|
+
|
|
160
|
+
Returns a promise resolving to the same client shape as `createReplaneClient` (`{ getConfig, close }`).
|
|
161
|
+
|
|
162
|
+
Notes:
|
|
163
|
+
|
|
164
|
+
- `getConfig(name)` resolves to the value from `initialData`.
|
|
165
|
+
- If a name is missing, it throws a `ReplaneError` (`Config not found: <name>`).
|
|
166
|
+
- The client works as usual but doesn't receive SSE updates (values remain whatever is in-memory).
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { createInMemoryReplaneClient } from "replane-sdk";
|
|
172
|
+
|
|
173
|
+
interface Configs {
|
|
174
|
+
"feature-a": boolean;
|
|
175
|
+
"max-items": { value: number; ttl: number };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const client = await createInMemoryReplaneClient<Configs>({
|
|
179
|
+
"feature-a": true,
|
|
180
|
+
"max-items": { value: 10, ttl: 3600 },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const featureA = client.getConfig("feature-a"); // TypeScript knows this is boolean
|
|
184
|
+
console.log(featureA); // true
|
|
185
|
+
|
|
186
|
+
const maxItems = client.getConfig("max-items"); // TypeScript knows the type
|
|
187
|
+
console.log(maxItems); // { value: 10, ttl: 3600 }
|
|
188
|
+
|
|
189
|
+
client.close();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `client.close()`
|
|
193
|
+
|
|
194
|
+
Gracefully shuts down the 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).
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// During shutdown
|
|
198
|
+
client.close();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Errors
|
|
202
|
+
|
|
203
|
+
`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.
|
|
204
|
+
|
|
205
|
+
The client receives realtime updates via SSE in the background. SSE connection errors are logged and automatically retried, but don't affect `getConfig` calls (which return the last known value).
|
|
206
|
+
|
|
207
|
+
## Environment notes
|
|
208
|
+
|
|
209
|
+
- Node 18+ has global `fetch`; for older Node versions supply `fetchFn`.
|
|
210
|
+
- Edge runtimes / Workers: provide a compatible `fetch` + `AbortController` if not built‑in.
|
|
211
|
+
|
|
212
|
+
## Common patterns
|
|
213
|
+
|
|
214
|
+
### Typed config
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
interface LayoutConfig {
|
|
218
|
+
variant: "a" | "b";
|
|
219
|
+
ttl: number;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface Configs {
|
|
223
|
+
layout: LayoutConfig;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const client = await createReplaneClient<Configs>({
|
|
227
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
228
|
+
baseUrl: "https://replane.my-host.com",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const layout = client.getConfig("layout"); // TypeScript knows this is LayoutConfig
|
|
232
|
+
console.log(layout); // { variant: "a", ttl: 3600 }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Context-based overrides
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
interface Configs {
|
|
239
|
+
"advanced-features": boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const client = await createReplaneClient<Configs>({
|
|
243
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
244
|
+
baseUrl: "https://replane.my-host.com",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Config has base value `false` but override: if `plan === "premium"` then `true`
|
|
248
|
+
|
|
249
|
+
// Free user
|
|
250
|
+
const freeUserEnabled = client.getConfig("advanced-features", {
|
|
251
|
+
context: { plan: "free" },
|
|
252
|
+
}); // false
|
|
253
|
+
|
|
254
|
+
// Premium user
|
|
255
|
+
const premiumUserEnabled = client.getConfig("advanced-features", {
|
|
256
|
+
context: { plan: "premium" },
|
|
257
|
+
}); // true
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Client-level context
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
interface Configs {
|
|
264
|
+
"feature-flag": boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const client = await createReplaneClient<Configs>({
|
|
268
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
269
|
+
baseUrl: "https://replane.my-host.com",
|
|
270
|
+
context: {
|
|
271
|
+
userId: "user-123",
|
|
272
|
+
region: "us-east",
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// This context is used for all configs unless overridden
|
|
277
|
+
const value1 = client.getConfig("feature-flag"); // Uses client-level context
|
|
278
|
+
const value2 = client.getConfig("feature-flag", {
|
|
279
|
+
context: { userId: "user-321" },
|
|
280
|
+
}); // Merges with client context
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Custom fetch (tests)
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const client = await createReplaneClient({
|
|
287
|
+
sdkKey: "TKN",
|
|
288
|
+
baseUrl: "https://api",
|
|
289
|
+
fetchFn: mockFetch,
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Required configs
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
interface Configs {
|
|
297
|
+
"api-key": string;
|
|
298
|
+
"database-url": string;
|
|
299
|
+
"optional-feature": boolean;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const client = await createReplaneClient<Configs>({
|
|
303
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
304
|
+
baseUrl: "https://replane.my-host.com",
|
|
305
|
+
requiredConfigs: {
|
|
306
|
+
"api-key": true,
|
|
307
|
+
"database-url": true,
|
|
308
|
+
"optional-feature": false, // Not required
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// If any required config is missing, initialization will throw
|
|
313
|
+
// Required configs that are deleted won't be removed (warning logged instead)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Fallback configs
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
interface Configs {
|
|
320
|
+
"feature-flag": boolean;
|
|
321
|
+
"max-connections": number;
|
|
322
|
+
"timeout-ms": number;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const client = await createReplaneClient<Configs>({
|
|
326
|
+
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
327
|
+
baseUrl: "https://replane.my-host.com",
|
|
328
|
+
fallbackConfigs: {
|
|
329
|
+
"feature-flag": false, // Use false if fetch fails
|
|
330
|
+
"max-connections": 10, // Use 10 if fetch fails
|
|
331
|
+
"timeout-ms": undefined, // No fallback - client.getConfig('timeout-ms') will throw if the initial fetch failed
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// If the initial fetch fails, fallback values are used
|
|
336
|
+
// Once the client connects, it will receive realtime updates
|
|
337
|
+
const maxConnections = client.getConfig("max-connections"); // 10 (or real value)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Multiple projects
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
interface ProjectAConfigs {
|
|
344
|
+
"feature-flag": boolean;
|
|
345
|
+
"max-users": number;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
interface ProjectBConfigs {
|
|
349
|
+
"feature-flag": boolean;
|
|
350
|
+
"api-rate-limit": number;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Each project needs its own SDK key and client instance
|
|
354
|
+
const projectAClient = await createReplaneClient<ProjectAConfigs>({
|
|
355
|
+
sdkKey: process.env.PROJECT_A_SDK_KEY!,
|
|
356
|
+
baseUrl: "https://replane.my-host.com",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const projectBClient = await createReplaneClient<ProjectBConfigs>({
|
|
360
|
+
sdkKey: process.env.PROJECT_B_SDK_KEY!,
|
|
361
|
+
baseUrl: "https://replane.my-host.com",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Each client only accesses configs from its respective project
|
|
365
|
+
const featureA = projectAClient.getConfig("feature-flag"); // boolean
|
|
366
|
+
const featureB = projectBClient.getConfig("feature-flag"); // boolean
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Roadmap
|
|
370
|
+
|
|
371
|
+
- Config caching
|
|
372
|
+
- Config invalidation
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
MIT
|