@replanejs/sdk 0.5.6 → 0.5.9
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 +159 -56
- package/dist/index.cjs +61 -17
- package/dist/index.d.ts +30 -13
- package/dist/index.js +58 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,11 +20,11 @@ You need: given a token + config name + optional context -> watch the value with
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm install
|
|
23
|
+
npm install @replanejs/sdk
|
|
24
24
|
# or
|
|
25
|
-
pnpm add
|
|
25
|
+
pnpm add @replanejs/sdk
|
|
26
26
|
# or
|
|
27
|
-
yarn add
|
|
27
|
+
yarn add @replanejs/sdk
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
@@ -32,7 +32,7 @@ yarn add replane-sdk
|
|
|
32
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
33
|
|
|
34
34
|
```ts
|
|
35
|
-
import { createReplaneClient } from "
|
|
35
|
+
import { createReplaneClient } from "@replanejs/sdk";
|
|
36
36
|
|
|
37
37
|
// Define your config types
|
|
38
38
|
interface Configs {
|
|
@@ -46,27 +46,27 @@ interface PasswordRequirements {
|
|
|
46
46
|
requireSymbol: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const configs = await createReplaneClient<Configs>({
|
|
50
50
|
// Each SDK key belongs to one project only
|
|
51
51
|
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
52
52
|
baseUrl: "https://replane.my-hosting.com",
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// Get a config value (knows about latest updates via SSE)
|
|
56
|
-
const featureFlag =
|
|
56
|
+
const featureFlag = configs.get("new-onboarding"); // Typed as boolean
|
|
57
57
|
|
|
58
58
|
if (featureFlag) {
|
|
59
59
|
console.log("New onboarding enabled!");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Typed config - no need to specify type again
|
|
63
|
-
const passwordReqs =
|
|
63
|
+
const passwordReqs = configs.get("password-requirements");
|
|
64
64
|
|
|
65
65
|
// Use the value directly
|
|
66
66
|
const { minLength } = passwordReqs; // TypeScript knows this is PasswordRequirements
|
|
67
67
|
|
|
68
68
|
// With context for override evaluation
|
|
69
|
-
const enabled =
|
|
69
|
+
const enabled = configs.get("billing-enabled", {
|
|
70
70
|
context: {
|
|
71
71
|
userId: "user-123",
|
|
72
72
|
plan: "premium",
|
|
@@ -79,35 +79,35 @@ if (enabled) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// When done, clean up resources
|
|
82
|
-
|
|
82
|
+
configs.close();
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
## API
|
|
86
86
|
|
|
87
87
|
### `createReplaneClient<T>(options)`
|
|
88
88
|
|
|
89
|
-
Returns a promise resolving to an object: `{
|
|
89
|
+
Returns a promise resolving to an object: `{ get, subscribe, close }`.
|
|
90
90
|
|
|
91
91
|
Type parameter `T` defines the shape of your configs (a mapping of config names to their value types).
|
|
92
92
|
|
|
93
|
-
`close()` stops the client and cleans up resources. After calling it, any subsequent call to `
|
|
93
|
+
`close()` stops the configs client and cleans up resources. After calling it, any subsequent call to `get` will throw. It is safe to call multiple times (no‑op after the first call).
|
|
94
94
|
|
|
95
95
|
#### Options
|
|
96
96
|
|
|
97
97
|
- `baseUrl` (string) – Replane origin (no trailing slash needed).
|
|
98
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
|
-
- `
|
|
100
|
-
- `
|
|
101
|
-
- `context` (object) – default context for all config evaluations. Can be overridden per-request in `
|
|
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
102
|
- `fetchFn` (function) – custom fetch (e.g. `undici.fetch` or mocked fetch in tests). Optional.
|
|
103
103
|
- `timeoutMs` (number) – abort the request after N ms. Default: 2000.
|
|
104
104
|
- `retries` (number) – number of retry attempts on failures (5xx or network errors). Default: 2.
|
|
105
105
|
- `retryDelayMs` (number) – base delay between retries in ms (a small jitter is applied). Default: 200.
|
|
106
106
|
- `logger` (object) – custom logger with `debug`, `info`, `warn`, `error` methods. Default: `console`.
|
|
107
107
|
|
|
108
|
-
### `
|
|
108
|
+
### `configs.get<K>(name, options?)`
|
|
109
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.
|
|
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
111
|
|
|
112
112
|
Parameters:
|
|
113
113
|
|
|
@@ -119,8 +119,7 @@ Returns the config value of type `T[K]` (synchronous). The return type is automa
|
|
|
119
119
|
|
|
120
120
|
Notes:
|
|
121
121
|
|
|
122
|
-
- The client receives realtime updates via SSE in the background.
|
|
123
|
-
- Values are automatically refreshed every 60 seconds as a fallback.
|
|
122
|
+
- The configs client receives realtime updates via SSE in the background.
|
|
124
123
|
- If the config is not found, throws a `ReplaneError` with code `not_found`.
|
|
125
124
|
- Context-based overrides are evaluated automatically based on context.
|
|
126
125
|
|
|
@@ -132,21 +131,81 @@ interface Configs {
|
|
|
132
131
|
"max-connections": number;
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
const
|
|
134
|
+
const configs = await createReplaneClient<Configs>({
|
|
136
135
|
sdkKey: "your-sdk-key",
|
|
137
136
|
baseUrl: "https://replane.my-host.com",
|
|
138
137
|
});
|
|
139
138
|
|
|
140
139
|
// Get value without context - TypeScript knows this is boolean
|
|
141
|
-
const enabled =
|
|
140
|
+
const enabled = configs.get("billing-enabled");
|
|
142
141
|
|
|
143
142
|
// Get value with context for override evaluation
|
|
144
|
-
const userEnabled =
|
|
143
|
+
const userEnabled = configs.get("billing-enabled", {
|
|
145
144
|
context: { userId: "user-123", plan: "premium" },
|
|
146
145
|
});
|
|
147
146
|
|
|
148
147
|
// Clean up when done
|
|
149
|
-
|
|
148
|
+
configs.close();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `configs.subscribe(callback)` or `configs.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 = configs.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 = configs.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 configs = 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 = configs.subscribe((config) => {
|
|
194
|
+
console.log(`Config ${config.name} updated:`, config.value);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Subscribe to a specific config
|
|
198
|
+
const unsubscribeFeature = configs.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
|
+
configs.close();
|
|
150
209
|
```
|
|
151
210
|
|
|
152
211
|
### `createInMemoryReplaneClient(initialData)`
|
|
@@ -157,52 +216,52 @@ Parameters:
|
|
|
157
216
|
|
|
158
217
|
- `initialData` (object) – map of config name to value.
|
|
159
218
|
|
|
160
|
-
Returns
|
|
219
|
+
Returns the same client shape as `createReplaneClient` (`{ get, subscribe, close }`).
|
|
161
220
|
|
|
162
221
|
Notes:
|
|
163
222
|
|
|
164
|
-
- `
|
|
223
|
+
- `get(name)` resolves to the value from `initialData`.
|
|
165
224
|
- If a name is missing, it throws a `ReplaneError` (`Config not found: <name>`).
|
|
166
225
|
- The client works as usual but doesn't receive SSE updates (values remain whatever is in-memory).
|
|
167
226
|
|
|
168
227
|
Example:
|
|
169
228
|
|
|
170
229
|
```ts
|
|
171
|
-
import { createInMemoryReplaneClient } from "
|
|
230
|
+
import { createInMemoryReplaneClient } from "@replanejs/sdk";
|
|
172
231
|
|
|
173
232
|
interface Configs {
|
|
174
233
|
"feature-a": boolean;
|
|
175
234
|
"max-items": { value: number; ttl: number };
|
|
176
235
|
}
|
|
177
236
|
|
|
178
|
-
const
|
|
237
|
+
const configs = createInMemoryReplaneClient<Configs>({
|
|
179
238
|
"feature-a": true,
|
|
180
239
|
"max-items": { value: 10, ttl: 3600 },
|
|
181
240
|
});
|
|
182
241
|
|
|
183
|
-
const featureA =
|
|
242
|
+
const featureA = configs.get("feature-a"); // TypeScript knows this is boolean
|
|
184
243
|
console.log(featureA); // true
|
|
185
244
|
|
|
186
|
-
const maxItems =
|
|
245
|
+
const maxItems = configs.get("max-items"); // TypeScript knows the type
|
|
187
246
|
console.log(maxItems); // { value: 10, ttl: 3600 }
|
|
188
247
|
|
|
189
|
-
|
|
248
|
+
configs.close();
|
|
190
249
|
```
|
|
191
250
|
|
|
192
|
-
### `
|
|
251
|
+
### `configs.close()`
|
|
193
252
|
|
|
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).
|
|
253
|
+
Gracefully shuts down the configs 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
254
|
|
|
196
255
|
```ts
|
|
197
256
|
// During shutdown
|
|
198
|
-
|
|
257
|
+
configs.close();
|
|
199
258
|
```
|
|
200
259
|
|
|
201
260
|
### Errors
|
|
202
261
|
|
|
203
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.
|
|
204
263
|
|
|
205
|
-
The client receives realtime updates via SSE in the background. SSE connection errors are logged and automatically retried, but don't affect `
|
|
264
|
+
The configs 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).
|
|
206
265
|
|
|
207
266
|
## Environment notes
|
|
208
267
|
|
|
@@ -223,12 +282,12 @@ interface Configs {
|
|
|
223
282
|
layout: LayoutConfig;
|
|
224
283
|
}
|
|
225
284
|
|
|
226
|
-
const
|
|
285
|
+
const configs = await createReplaneClient<Configs>({
|
|
227
286
|
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
228
287
|
baseUrl: "https://replane.my-host.com",
|
|
229
288
|
});
|
|
230
289
|
|
|
231
|
-
const layout =
|
|
290
|
+
const layout = configs.get("layout"); // TypeScript knows this is LayoutConfig
|
|
232
291
|
console.log(layout); // { variant: "a", ttl: 3600 }
|
|
233
292
|
```
|
|
234
293
|
|
|
@@ -239,7 +298,7 @@ interface Configs {
|
|
|
239
298
|
"advanced-features": boolean;
|
|
240
299
|
}
|
|
241
300
|
|
|
242
|
-
const
|
|
301
|
+
const configs = await createReplaneClient<Configs>({
|
|
243
302
|
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
244
303
|
baseUrl: "https://replane.my-host.com",
|
|
245
304
|
});
|
|
@@ -247,12 +306,12 @@ const client = await createReplaneClient<Configs>({
|
|
|
247
306
|
// Config has base value `false` but override: if `plan === "premium"` then `true`
|
|
248
307
|
|
|
249
308
|
// Free user
|
|
250
|
-
const freeUserEnabled =
|
|
309
|
+
const freeUserEnabled = configs.get("advanced-features", {
|
|
251
310
|
context: { plan: "free" },
|
|
252
311
|
}); // false
|
|
253
312
|
|
|
254
313
|
// Premium user
|
|
255
|
-
const premiumUserEnabled =
|
|
314
|
+
const premiumUserEnabled = configs.get("advanced-features", {
|
|
256
315
|
context: { plan: "premium" },
|
|
257
316
|
}); // true
|
|
258
317
|
```
|
|
@@ -264,7 +323,7 @@ interface Configs {
|
|
|
264
323
|
"feature-flag": boolean;
|
|
265
324
|
}
|
|
266
325
|
|
|
267
|
-
const
|
|
326
|
+
const configs = await createReplaneClient<Configs>({
|
|
268
327
|
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
269
328
|
baseUrl: "https://replane.my-host.com",
|
|
270
329
|
context: {
|
|
@@ -274,8 +333,8 @@ const client = await createReplaneClient<Configs>({
|
|
|
274
333
|
});
|
|
275
334
|
|
|
276
335
|
// This context is used for all configs unless overridden
|
|
277
|
-
const value1 =
|
|
278
|
-
const value2 =
|
|
336
|
+
const value1 = configs.get("feature-flag"); // Uses client-level context
|
|
337
|
+
const value2 = configs.get("feature-flag", {
|
|
279
338
|
context: { userId: "user-321" },
|
|
280
339
|
}); // Merges with client context
|
|
281
340
|
```
|
|
@@ -283,7 +342,7 @@ const value2 = client.getConfig("feature-flag", {
|
|
|
283
342
|
### Custom fetch (tests)
|
|
284
343
|
|
|
285
344
|
```ts
|
|
286
|
-
const
|
|
345
|
+
const configs = await createReplaneClient({
|
|
287
346
|
sdkKey: "TKN",
|
|
288
347
|
baseUrl: "https://api",
|
|
289
348
|
fetchFn: mockFetch,
|
|
@@ -299,18 +358,20 @@ interface Configs {
|
|
|
299
358
|
"optional-feature": boolean;
|
|
300
359
|
}
|
|
301
360
|
|
|
302
|
-
const
|
|
361
|
+
const configs = await createReplaneClient<Configs>({
|
|
303
362
|
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
304
363
|
baseUrl: "https://replane.my-host.com",
|
|
305
|
-
|
|
364
|
+
required: {
|
|
306
365
|
"api-key": true,
|
|
307
366
|
"database-url": true,
|
|
308
367
|
"optional-feature": false, // Not required
|
|
309
368
|
},
|
|
310
369
|
});
|
|
311
370
|
|
|
371
|
+
// Alternative: use an array
|
|
372
|
+
// required: ["api-key", "database-url"]
|
|
373
|
+
|
|
312
374
|
// If any required config is missing, initialization will throw
|
|
313
|
-
// Required configs that are deleted won't be removed (warning logged instead)
|
|
314
375
|
```
|
|
315
376
|
|
|
316
377
|
### Fallback configs
|
|
@@ -322,19 +383,19 @@ interface Configs {
|
|
|
322
383
|
"timeout-ms": number;
|
|
323
384
|
}
|
|
324
385
|
|
|
325
|
-
const
|
|
386
|
+
const configs = await createReplaneClient<Configs>({
|
|
326
387
|
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
327
388
|
baseUrl: "https://replane.my-host.com",
|
|
328
|
-
|
|
389
|
+
fallbacks: {
|
|
329
390
|
"feature-flag": false, // Use false if fetch fails
|
|
330
391
|
"max-connections": 10, // Use 10 if fetch fails
|
|
331
|
-
"timeout-ms":
|
|
392
|
+
"timeout-ms": 5000, // Use 5s if fetch fails
|
|
332
393
|
},
|
|
333
394
|
});
|
|
334
395
|
|
|
335
396
|
// If the initial fetch fails, fallback values are used
|
|
336
|
-
// Once the client connects, it will receive realtime updates
|
|
337
|
-
const maxConnections =
|
|
397
|
+
// Once the configs client connects, it will receive realtime updates
|
|
398
|
+
const maxConnections = configs.get("max-connections"); // 10 (or real value)
|
|
338
399
|
```
|
|
339
400
|
|
|
340
401
|
### Multiple projects
|
|
@@ -350,20 +411,62 @@ interface ProjectBConfigs {
|
|
|
350
411
|
"api-rate-limit": number;
|
|
351
412
|
}
|
|
352
413
|
|
|
353
|
-
// Each project needs its own SDK key and client instance
|
|
354
|
-
const
|
|
414
|
+
// Each project needs its own SDK key and configs client instance
|
|
415
|
+
const projectAConfigs = await createReplaneClient<ProjectAConfigs>({
|
|
355
416
|
sdkKey: process.env.PROJECT_A_SDK_KEY!,
|
|
356
417
|
baseUrl: "https://replane.my-host.com",
|
|
357
418
|
});
|
|
358
419
|
|
|
359
|
-
const
|
|
420
|
+
const projectBConfigs = await createReplaneClient<ProjectBConfigs>({
|
|
360
421
|
sdkKey: process.env.PROJECT_B_SDK_KEY!,
|
|
361
422
|
baseUrl: "https://replane.my-host.com",
|
|
362
423
|
});
|
|
363
424
|
|
|
364
|
-
// Each client only accesses configs from its respective project
|
|
365
|
-
const featureA =
|
|
366
|
-
const featureB =
|
|
425
|
+
// Each configs 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 configs = 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 = configs.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 = configs.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 = configs.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
|
+
configs.close();
|
|
367
470
|
```
|
|
368
471
|
|
|
369
472
|
## Roadmap
|
package/dist/index.cjs
CHANGED
|
@@ -270,7 +270,7 @@ async function createReplaneClient(sdkOptions) {
|
|
|
270
270
|
}
|
|
271
271
|
function createInMemoryReplaneClient(initialData) {
|
|
272
272
|
return {
|
|
273
|
-
|
|
273
|
+
get: (configName) => {
|
|
274
274
|
const config = initialData[configName];
|
|
275
275
|
if (config === void 0) {
|
|
276
276
|
throw new ReplaneError({
|
|
@@ -280,16 +280,22 @@ function createInMemoryReplaneClient(initialData) {
|
|
|
280
280
|
}
|
|
281
281
|
return config;
|
|
282
282
|
},
|
|
283
|
+
subscribe: () => {
|
|
284
|
+
return () => {
|
|
285
|
+
};
|
|
286
|
+
},
|
|
283
287
|
close: () => {
|
|
284
288
|
}
|
|
285
289
|
};
|
|
286
290
|
}
|
|
287
291
|
async function _createReplaneClient(sdkOptions, storage) {
|
|
288
292
|
if (!sdkOptions.sdkKey) throw new Error("SDK key is required");
|
|
289
|
-
|
|
293
|
+
const configs = new Map(
|
|
290
294
|
sdkOptions.fallbacks.map((config) => [config.name, config])
|
|
291
295
|
);
|
|
292
296
|
const clientReady = new Deferred();
|
|
297
|
+
const configSubscriptions = /* @__PURE__ */ new Map();
|
|
298
|
+
const clientSubscriptions = /* @__PURE__ */ new Set();
|
|
293
299
|
(async () => {
|
|
294
300
|
try {
|
|
295
301
|
const replicationStream = storage.startReplicationStream({
|
|
@@ -305,26 +311,30 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
305
311
|
})
|
|
306
312
|
});
|
|
307
313
|
for await (const event of replicationStream) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
value: event.value
|
|
314
|
+
const updatedConfigs = event.type === "config_change" ? [event] : event.configs;
|
|
315
|
+
for (const config of updatedConfigs) {
|
|
316
|
+
if (config.version <= (configs.get(config.name)?.version ?? -1)) continue;
|
|
317
|
+
configs.set(config.name, {
|
|
318
|
+
name: config.name,
|
|
319
|
+
overrides: config.overrides,
|
|
320
|
+
version: config.version,
|
|
321
|
+
value: config.value
|
|
317
322
|
});
|
|
318
|
-
|
|
319
|
-
|
|
323
|
+
for (const callback of clientSubscriptions) {
|
|
324
|
+
callback({ name: config.name, value: config.value });
|
|
325
|
+
}
|
|
326
|
+
for (const callback of configSubscriptions.get(config.name) ?? []) {
|
|
327
|
+
callback({ name: config.name, value: config.value });
|
|
328
|
+
}
|
|
320
329
|
}
|
|
330
|
+
clientReady.resolve();
|
|
321
331
|
}
|
|
322
332
|
} catch (error) {
|
|
323
333
|
sdkOptions.logger.error("Replane: error initializing client:", error);
|
|
324
334
|
clientReady.reject(error);
|
|
325
335
|
}
|
|
326
336
|
})();
|
|
327
|
-
function
|
|
337
|
+
function get(configName, getConfigOptions = {}) {
|
|
328
338
|
const config = configs.get(String(configName));
|
|
329
339
|
if (config === void 0) {
|
|
330
340
|
throw new ReplaneError({
|
|
@@ -347,6 +357,39 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
347
357
|
return config.value;
|
|
348
358
|
}
|
|
349
359
|
}
|
|
360
|
+
const subscribe = (callbackOrConfigName, callbackOrUndefined) => {
|
|
361
|
+
let configName = void 0;
|
|
362
|
+
let callback;
|
|
363
|
+
if (typeof callbackOrConfigName === "function") {
|
|
364
|
+
callback = callbackOrConfigName;
|
|
365
|
+
} else {
|
|
366
|
+
configName = callbackOrConfigName;
|
|
367
|
+
if (callbackOrUndefined === void 0) {
|
|
368
|
+
throw new Error("callback is required when config name is provided");
|
|
369
|
+
}
|
|
370
|
+
callback = callbackOrUndefined;
|
|
371
|
+
}
|
|
372
|
+
const originalCallback = callback;
|
|
373
|
+
callback = (...args) => {
|
|
374
|
+
originalCallback(...args);
|
|
375
|
+
};
|
|
376
|
+
if (configName === void 0) {
|
|
377
|
+
clientSubscriptions.add(callback);
|
|
378
|
+
return () => {
|
|
379
|
+
clientSubscriptions.delete(callback);
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (!configSubscriptions.has(configName)) {
|
|
383
|
+
configSubscriptions.set(configName, /* @__PURE__ */ new Set());
|
|
384
|
+
}
|
|
385
|
+
configSubscriptions.get(configName).add(callback);
|
|
386
|
+
return () => {
|
|
387
|
+
configSubscriptions.get(configName)?.delete(callback);
|
|
388
|
+
if (configSubscriptions.get(configName)?.size === 0) {
|
|
389
|
+
configSubscriptions.delete(configName);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
};
|
|
350
393
|
const close = () => storage.close();
|
|
351
394
|
const initializationTimeoutId = setTimeout(() => {
|
|
352
395
|
if (sdkOptions.fallbacks.length === 0) {
|
|
@@ -376,11 +419,12 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
376
419
|
return;
|
|
377
420
|
}
|
|
378
421
|
clientReady.resolve();
|
|
379
|
-
}, sdkOptions.
|
|
422
|
+
}, sdkOptions.initializationTimeoutMs);
|
|
380
423
|
clientReady.promise.then(() => clearTimeout(initializationTimeoutId));
|
|
381
424
|
await clientReady.promise;
|
|
382
425
|
return {
|
|
383
|
-
|
|
426
|
+
get,
|
|
427
|
+
subscribe,
|
|
384
428
|
close
|
|
385
429
|
};
|
|
386
430
|
}
|
|
@@ -391,7 +435,7 @@ function toFinalOptions(defaults) {
|
|
|
391
435
|
fetchFn: defaults.fetchFn ?? // some browsers require binding the fetch function to window
|
|
392
436
|
globalThis.fetch.bind(globalThis),
|
|
393
437
|
requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
|
|
394
|
-
|
|
438
|
+
initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5e3,
|
|
395
439
|
logger: defaults.logger ?? console,
|
|
396
440
|
retryDelayMs: defaults.retryDelayMs ?? 200,
|
|
397
441
|
context: {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,11 +3,18 @@ type Configs = object;
|
|
|
3
3
|
type ReplaneContext = Record<string, unknown>;
|
|
4
4
|
interface ReplaneClientOptions<T extends Configs> {
|
|
5
5
|
/**
|
|
6
|
-
* Base URL of the Replane
|
|
6
|
+
* Base URL of the Replane instance (no trailing slash).
|
|
7
|
+
* @example
|
|
8
|
+
* "https://app.replane.dev"
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* "https://replane.yourdomain.com"
|
|
7
12
|
*/
|
|
8
13
|
baseUrl: string;
|
|
9
14
|
/**
|
|
10
15
|
* Project SDK key for authorization.
|
|
16
|
+
* @example
|
|
17
|
+
* "rp_XXXXXXXXX"
|
|
11
18
|
*/
|
|
12
19
|
sdkKey: string;
|
|
13
20
|
/**
|
|
@@ -23,10 +30,10 @@ interface ReplaneClientOptions<T extends Configs> {
|
|
|
23
30
|
* Optional timeout in ms for the SDK initialization.
|
|
24
31
|
* @default 5000
|
|
25
32
|
*/
|
|
26
|
-
|
|
33
|
+
initializationTimeoutMs?: number;
|
|
27
34
|
/**
|
|
28
35
|
* Delay between retries in ms.
|
|
29
|
-
* @default
|
|
36
|
+
* @default 200
|
|
30
37
|
*/
|
|
31
38
|
retryDelayMs?: number;
|
|
32
39
|
/**
|
|
@@ -35,7 +42,7 @@ interface ReplaneClientOptions<T extends Configs> {
|
|
|
35
42
|
logger?: ReplaneLogger;
|
|
36
43
|
/**
|
|
37
44
|
* Default context for all config evaluations.
|
|
38
|
-
* Can be overridden per-request in `client.
|
|
45
|
+
* Can be overridden per-request in `client.get()`.
|
|
39
46
|
*/
|
|
40
47
|
context?: ReplaneContext;
|
|
41
48
|
/**
|
|
@@ -57,7 +64,8 @@ interface ReplaneClientOptions<T extends Configs> {
|
|
|
57
64
|
*/
|
|
58
65
|
required?: { [K in keyof T]: boolean } | Array<keyof T>;
|
|
59
66
|
/**
|
|
60
|
-
* Fallback
|
|
67
|
+
* Fallback values to use if the initial request to fetch configs fails.
|
|
68
|
+
* When provided, all configs must be specified.
|
|
61
69
|
* @example
|
|
62
70
|
* {
|
|
63
71
|
* fallbacks: {
|
|
@@ -80,15 +88,24 @@ interface GetConfigOptions {
|
|
|
80
88
|
*/
|
|
81
89
|
context?: ReplaneContext;
|
|
82
90
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
close(): void;
|
|
88
|
-
}
|
|
91
|
+
type MapConfig<T extends Configs> = { [K in keyof T]: {
|
|
92
|
+
name: K;
|
|
93
|
+
value: T[K];
|
|
94
|
+
} }[keyof T];
|
|
89
95
|
interface ReplaneClient<T extends Configs> {
|
|
90
96
|
/** Get a config by its name. */
|
|
91
|
-
|
|
97
|
+
get<K extends keyof T>(configName: K, options?: GetConfigOptions): T[K];
|
|
98
|
+
/** Subscribe to config changes.
|
|
99
|
+
* @param callback - A function to call when an config is changed. The callback will be called with the new config value.
|
|
100
|
+
* @returns A function to unsubscribe from the config changes.
|
|
101
|
+
*/
|
|
102
|
+
subscribe(callback: (config: MapConfig<T>) => void): () => void;
|
|
103
|
+
/** Subscribe to a specific config change.
|
|
104
|
+
* @param configName - The name of the config to subscribe to.
|
|
105
|
+
* @param callback - A function to call when the config is changed. The callback will be called with the new config value.
|
|
106
|
+
* @returns A function to unsubscribe from the config changes.
|
|
107
|
+
*/
|
|
108
|
+
subscribe<K extends keyof T>(configName: K, callback: (config: MapConfig<Pick<T, K>>) => void): () => void;
|
|
92
109
|
/** Close the client and clean up resources. */
|
|
93
110
|
close(): void;
|
|
94
111
|
}
|
|
@@ -115,4 +132,4 @@ declare function createReplaneClient<T extends Configs = Record<string, unknown>
|
|
|
115
132
|
*/
|
|
116
133
|
declare function createInMemoryReplaneClient<T extends Configs = Record<string, unknown>>(initialData: T): ReplaneClient<T>;
|
|
117
134
|
//#endregion
|
|
118
|
-
export {
|
|
135
|
+
export { GetConfigOptions, ReplaneClient, ReplaneClientOptions, ReplaneContext, ReplaneError, ReplaneLogger, createInMemoryReplaneClient, createReplaneClient };
|
package/dist/index.js
CHANGED
|
@@ -231,7 +231,7 @@ async function createReplaneClient(sdkOptions) {
|
|
|
231
231
|
*/
|
|
232
232
|
function createInMemoryReplaneClient(initialData) {
|
|
233
233
|
return {
|
|
234
|
-
|
|
234
|
+
get: (configName) => {
|
|
235
235
|
const config = initialData[configName];
|
|
236
236
|
if (config === void 0) throw new ReplaneError({
|
|
237
237
|
message: `Config not found: ${String(configName)}`,
|
|
@@ -239,13 +239,18 @@ function createInMemoryReplaneClient(initialData) {
|
|
|
239
239
|
});
|
|
240
240
|
return config;
|
|
241
241
|
},
|
|
242
|
+
subscribe: () => {
|
|
243
|
+
return () => {};
|
|
244
|
+
},
|
|
242
245
|
close: () => {}
|
|
243
246
|
};
|
|
244
247
|
}
|
|
245
248
|
async function _createReplaneClient(sdkOptions, storage) {
|
|
246
249
|
if (!sdkOptions.sdkKey) throw new Error("SDK key is required");
|
|
247
|
-
|
|
250
|
+
const configs = new Map(sdkOptions.fallbacks.map((config) => [config.name, config]));
|
|
248
251
|
const clientReady = new Deferred();
|
|
252
|
+
const configSubscriptions = new Map();
|
|
253
|
+
const clientSubscriptions = new Set();
|
|
249
254
|
(async () => {
|
|
250
255
|
try {
|
|
251
256
|
const replicationStream = storage.startReplicationStream({
|
|
@@ -260,22 +265,33 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
260
265
|
requiredConfigs: sdkOptions.requiredConfigs
|
|
261
266
|
})
|
|
262
267
|
});
|
|
263
|
-
for await (const event of replicationStream)
|
|
264
|
-
|
|
268
|
+
for await (const event of replicationStream) {
|
|
269
|
+
const updatedConfigs = event.type === "config_change" ? [event] : event.configs;
|
|
270
|
+
for (const config of updatedConfigs) {
|
|
271
|
+
if (config.version <= (configs.get(config.name)?.version ?? -1)) continue;
|
|
272
|
+
configs.set(config.name, {
|
|
273
|
+
name: config.name,
|
|
274
|
+
overrides: config.overrides,
|
|
275
|
+
version: config.version,
|
|
276
|
+
value: config.value
|
|
277
|
+
});
|
|
278
|
+
for (const callback of clientSubscriptions) callback({
|
|
279
|
+
name: config.name,
|
|
280
|
+
value: config.value
|
|
281
|
+
});
|
|
282
|
+
for (const callback of configSubscriptions.get(config.name) ?? []) callback({
|
|
283
|
+
name: config.name,
|
|
284
|
+
value: config.value
|
|
285
|
+
});
|
|
286
|
+
}
|
|
265
287
|
clientReady.resolve();
|
|
266
|
-
}
|
|
267
|
-
name: event.configName,
|
|
268
|
-
overrides: event.overrides,
|
|
269
|
-
version: event.version,
|
|
270
|
-
value: event.value
|
|
271
|
-
});
|
|
272
|
-
else warnNever(event, sdkOptions.logger, "Replane: unknown event type in event stream");
|
|
288
|
+
}
|
|
273
289
|
} catch (error) {
|
|
274
290
|
sdkOptions.logger.error("Replane: error initializing client:", error);
|
|
275
291
|
clientReady.reject(error);
|
|
276
292
|
}
|
|
277
293
|
})();
|
|
278
|
-
function
|
|
294
|
+
function get(configName, getConfigOptions = {}) {
|
|
279
295
|
const config = configs.get(String(configName));
|
|
280
296
|
if (config === void 0) throw new ReplaneError({
|
|
281
297
|
message: `Config not found: ${String(configName)}`,
|
|
@@ -291,6 +307,32 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
291
307
|
return config.value;
|
|
292
308
|
}
|
|
293
309
|
}
|
|
310
|
+
const subscribe = (callbackOrConfigName, callbackOrUndefined) => {
|
|
311
|
+
let configName = void 0;
|
|
312
|
+
let callback;
|
|
313
|
+
if (typeof callbackOrConfigName === "function") callback = callbackOrConfigName;
|
|
314
|
+
else {
|
|
315
|
+
configName = callbackOrConfigName;
|
|
316
|
+
if (callbackOrUndefined === void 0) throw new Error("callback is required when config name is provided");
|
|
317
|
+
callback = callbackOrUndefined;
|
|
318
|
+
}
|
|
319
|
+
const originalCallback = callback;
|
|
320
|
+
callback = (...args) => {
|
|
321
|
+
originalCallback(...args);
|
|
322
|
+
};
|
|
323
|
+
if (configName === void 0) {
|
|
324
|
+
clientSubscriptions.add(callback);
|
|
325
|
+
return () => {
|
|
326
|
+
clientSubscriptions.delete(callback);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (!configSubscriptions.has(configName)) configSubscriptions.set(configName, new Set());
|
|
330
|
+
configSubscriptions.get(configName).add(callback);
|
|
331
|
+
return () => {
|
|
332
|
+
configSubscriptions.get(configName)?.delete(callback);
|
|
333
|
+
if (configSubscriptions.get(configName)?.size === 0) configSubscriptions.delete(configName);
|
|
334
|
+
};
|
|
335
|
+
};
|
|
294
336
|
const close = () => storage.close();
|
|
295
337
|
const initializationTimeoutId = setTimeout(() => {
|
|
296
338
|
if (sdkOptions.fallbacks.length === 0) {
|
|
@@ -312,11 +354,12 @@ async function _createReplaneClient(sdkOptions, storage) {
|
|
|
312
354
|
return;
|
|
313
355
|
}
|
|
314
356
|
clientReady.resolve();
|
|
315
|
-
}, sdkOptions.
|
|
357
|
+
}, sdkOptions.initializationTimeoutMs);
|
|
316
358
|
clientReady.promise.then(() => clearTimeout(initializationTimeoutId));
|
|
317
359
|
await clientReady.promise;
|
|
318
360
|
return {
|
|
319
|
-
|
|
361
|
+
get,
|
|
362
|
+
subscribe,
|
|
320
363
|
close
|
|
321
364
|
};
|
|
322
365
|
}
|
|
@@ -326,7 +369,7 @@ function toFinalOptions(defaults) {
|
|
|
326
369
|
baseUrl: defaults.baseUrl.replace(/\/+$/, ""),
|
|
327
370
|
fetchFn: defaults.fetchFn ?? globalThis.fetch.bind(globalThis),
|
|
328
371
|
requestTimeoutMs: defaults.requestTimeoutMs ?? 2e3,
|
|
329
|
-
|
|
372
|
+
initializationTimeoutMs: defaults.initializationTimeoutMs ?? 5e3,
|
|
330
373
|
logger: defaults.logger ?? console,
|
|
331
374
|
retryDelayMs: defaults.retryDelayMs ?? 200,
|
|
332
375
|
context: { ...defaults.context ?? {} },
|