@replanejs/sdk 0.9.2 → 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 +36 -58
- package/dist/index.cjs +319 -26
- package/dist/index.d.cts +166 -130
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +166 -130
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +319 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Replane JavaScript SDK</h1>
|
|
2
|
+
<p align="center">Dynamic configuration for Node.js, Deno, Bun, and browsers.</p>
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
<p align="center">
|
|
5
|
+
<a href="https://cloud.replane.dev"><img src="https://img.shields.io/badge/Try-Replane%20Cloud-blue" alt="Replane Cloud"></a>
|
|
6
|
+
<a href="https://www.npmjs.com/package/@replanejs/sdk"><img src="https://img.shields.io/npm/v/@replanejs/sdk" alt="npm"></a>
|
|
7
|
+
<a href="https://github.com/replane-dev/replane-javascript/blob/main/LICENSE"><img src="https://img.shields.io/github/license/replane-dev/replane-javascript" alt="License"></a>
|
|
8
|
+
<a href="https://github.com/orgs/replane-dev/discussions"><img src="https://img.shields.io/badge/discussions-join-blue?logo=github" alt="Community"></a>
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
<picture>
|
|
12
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/replane-dev/replane/main/public/replane-window-screenshot-dark-v1.png">
|
|
13
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/replane-dev/replane/main/public/replane-window-screenshot-light-with-border-v2.jpg">
|
|
14
|
+
<img alt="Replane Screenshot" src="https://raw.githubusercontent.com/replane-dev/replane/main/public/replane-window-screenshot-light-with-border-v2.jpg">
|
|
15
|
+
</picture>
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
[Replane](https://github.com/replane-dev/replane) is a dynamic configuration manager. Store feature flags, app settings, and operational config in one place—with version history, optional approvals, and realtime sync to your services. No redeploys needed.
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
## Why Dynamic Configuration?
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
- **Feature flags** – toggle features, run A/B tests, roll out to user segments
|
|
22
|
+
- **Operational tuning** – adjust limits, TTLs, and timeouts without redeploying
|
|
23
|
+
- **Per-environment settings** – different values for production, staging, dev
|
|
24
|
+
- **Incident response** – instantly revert to a known-good version
|
|
25
|
+
- **Cross-service configuration** – share settings with realtime sync
|
|
26
|
+
- **Non-engineer access** – safe editing with schema validation
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
## Features
|
|
16
29
|
|
|
17
30
|
- Works in ESM and CJS (dual build)
|
|
18
31
|
- Zero runtime deps (uses native `fetch` — bring a polyfill if your runtime lacks it)
|
|
@@ -33,8 +46,6 @@ yarn add @replanejs/sdk
|
|
|
33
46
|
|
|
34
47
|
## Quick start
|
|
35
48
|
|
|
36
|
-
> **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.
|
|
37
|
-
|
|
38
49
|
```ts
|
|
39
50
|
import { Replane } from "@replanejs/sdk";
|
|
40
51
|
|
|
@@ -53,14 +64,17 @@ interface PasswordRequirements {
|
|
|
53
64
|
// Create the client with optional constructor options
|
|
54
65
|
const replane = new Replane<Configs>({
|
|
55
66
|
context: {
|
|
56
|
-
|
|
67
|
+
// example context
|
|
68
|
+
userId: "user-123",
|
|
69
|
+
plan: "premium",
|
|
70
|
+
region: "us-east",
|
|
57
71
|
},
|
|
58
72
|
});
|
|
59
73
|
|
|
60
74
|
// Connect to the server
|
|
61
75
|
await replane.connect({
|
|
62
76
|
sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
63
|
-
baseUrl: "https://replane.
|
|
77
|
+
baseUrl: "https://cloud.replane.dev", // or your self-hosted URL
|
|
64
78
|
});
|
|
65
79
|
|
|
66
80
|
// Get a config value (knows about latest updates via SSE)
|
|
@@ -79,9 +93,8 @@ const { minLength } = passwordReqs; // TypeScript knows this is PasswordRequirem
|
|
|
79
93
|
// With context for override evaluation
|
|
80
94
|
const enabled = replane.get("billing-enabled", {
|
|
81
95
|
context: {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
region: "us-east",
|
|
96
|
+
plan: "free",
|
|
97
|
+
deviceType: "mobile",
|
|
85
98
|
},
|
|
86
99
|
});
|
|
87
100
|
|
|
@@ -158,7 +171,7 @@ interface Configs {
|
|
|
158
171
|
const replane = new Replane<Configs>();
|
|
159
172
|
await replane.connect({
|
|
160
173
|
sdkKey: "your-sdk-key",
|
|
161
|
-
baseUrl: "https://replane.
|
|
174
|
+
baseUrl: "https://cloud.replane.dev", // or your self-hosted URL
|
|
162
175
|
});
|
|
163
176
|
|
|
164
177
|
// Get value without context - TypeScript knows this is boolean
|
|
@@ -176,31 +189,14 @@ const maxConnections = replane.get("max-connections", { default: 10 });
|
|
|
176
189
|
replane.disconnect();
|
|
177
190
|
```
|
|
178
191
|
|
|
179
|
-
### `replane.subscribe(
|
|
180
|
-
|
|
181
|
-
Subscribe to config changes and receive real-time updates when configs are modified.
|
|
192
|
+
### `replane.subscribe(configName, callback)`
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
1. **Subscribe to all config changes:**
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
const unsubscribe = replane.subscribe((config) => {
|
|
189
|
-
console.log(`Config ${config.name} changed to:`, config.value);
|
|
190
|
-
});
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
2. **Subscribe to a specific config:**
|
|
194
|
-
```ts
|
|
195
|
-
const unsubscribe = replane.subscribe("billing-enabled", (config) => {
|
|
196
|
-
console.log(`billing-enabled changed to:`, config.value);
|
|
197
|
-
});
|
|
198
|
-
```
|
|
194
|
+
Subscribe to a specific config's changes and receive real-time updates when it is modified.
|
|
199
195
|
|
|
200
196
|
Parameters:
|
|
201
197
|
|
|
202
|
-
- `
|
|
203
|
-
- `
|
|
198
|
+
- `configName` (K extends keyof T) – The config to watch for changes.
|
|
199
|
+
- `callback` (function) – Function called when the config changes. Receives an object with `{ name, value }`.
|
|
204
200
|
|
|
205
201
|
Returns a function to unsubscribe from the config changes.
|
|
206
202
|
|
|
@@ -215,12 +211,7 @@ interface Configs {
|
|
|
215
211
|
const replane = new Replane<Configs>();
|
|
216
212
|
await replane.connect({
|
|
217
213
|
sdkKey: "your-sdk-key",
|
|
218
|
-
baseUrl: "https://replane.
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Subscribe to all config changes
|
|
222
|
-
const unsubscribeAll = replane.subscribe((config) => {
|
|
223
|
-
console.log(`Config ${config.name} updated:`, config.value);
|
|
214
|
+
baseUrl: "https://cloud.replane.dev", // or your self-hosted URL
|
|
224
215
|
});
|
|
225
216
|
|
|
226
217
|
// Subscribe to a specific config
|
|
@@ -230,7 +221,6 @@ const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
|
|
|
230
221
|
});
|
|
231
222
|
|
|
232
223
|
// Later: unsubscribe when done
|
|
233
|
-
unsubscribeAll();
|
|
234
224
|
unsubscribeFeature();
|
|
235
225
|
|
|
236
226
|
// Clean up when done
|
|
@@ -448,30 +438,18 @@ await replane.connect({
|
|
|
448
438
|
baseUrl: "https://replane.my-host.com",
|
|
449
439
|
});
|
|
450
440
|
|
|
451
|
-
// Subscribe to
|
|
452
|
-
const unsubscribeAll = replane.subscribe((config) => {
|
|
453
|
-
console.log(`Config ${config.name} changed:`, config.value);
|
|
454
|
-
|
|
455
|
-
// React to specific config changes
|
|
456
|
-
if (config.name === "feature-flag") {
|
|
457
|
-
console.log("Feature flag updated:", config.value);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
// Subscribe to a specific config only
|
|
441
|
+
// Subscribe to specific configs
|
|
462
442
|
const unsubscribeFeature = replane.subscribe("feature-flag", (config) => {
|
|
463
443
|
console.log("Feature flag changed:", config.value);
|
|
464
444
|
// config.value is automatically typed as boolean
|
|
465
445
|
});
|
|
466
446
|
|
|
467
|
-
// Subscribe to multiple specific configs
|
|
468
447
|
const unsubscribeMaxUsers = replane.subscribe("max-users", (config) => {
|
|
469
448
|
console.log("Max users changed:", config.value);
|
|
470
449
|
// config.value is automatically typed as number
|
|
471
450
|
});
|
|
472
451
|
|
|
473
452
|
// Cleanup
|
|
474
|
-
unsubscribeAll();
|
|
475
453
|
unsubscribeFeature();
|
|
476
454
|
unsubscribeMaxUsers();
|
|
477
455
|
replane.disconnect();
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,20 @@ var ReplaneError = class extends Error {
|
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/utils.ts
|
|
33
33
|
/**
|
|
34
|
+
* Generates a random UUID using the Web Crypto API.
|
|
35
|
+
* Falls back to a simple implementation if crypto.randomUUID is not available.
|
|
36
|
+
*
|
|
37
|
+
* @returns A random UUID string
|
|
38
|
+
*/
|
|
39
|
+
function generateClientId() {
|
|
40
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
41
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
42
|
+
const r = Math.random() * 16 | 0;
|
|
43
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
44
|
+
return v.toString(16);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
34
48
|
* Returns a promise that resolves after the specified delay
|
|
35
49
|
*
|
|
36
50
|
* @param ms - Delay in milliseconds
|
|
@@ -482,12 +496,21 @@ function castToContextType(expectedValue, contextValue) {
|
|
|
482
496
|
|
|
483
497
|
//#endregion
|
|
484
498
|
//#region src/version.ts
|
|
485
|
-
const VERSION = "0.
|
|
499
|
+
const VERSION = "1.0.0";
|
|
486
500
|
const DEFAULT_AGENT = `replane-js-sdk/${VERSION}`;
|
|
487
501
|
|
|
488
502
|
//#endregion
|
|
489
503
|
//#region src/client.ts
|
|
490
504
|
/**
|
|
505
|
+
* The context key for the auto-generated client ID.
|
|
506
|
+
* This key is automatically set by the SDK and can be used for segmentation.
|
|
507
|
+
* User-provided values for this key take precedence over the auto-generated value.
|
|
508
|
+
*/
|
|
509
|
+
const REPLANE_CLIENT_ID_KEY = "replaneClientId";
|
|
510
|
+
function asReplaneHandle(replane) {
|
|
511
|
+
return replane;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
491
514
|
* The Replane client for managing dynamic configuration.
|
|
492
515
|
*
|
|
493
516
|
* @example
|
|
@@ -526,12 +549,31 @@ const DEFAULT_AGENT = `replane-js-sdk/${VERSION}`;
|
|
|
526
549
|
* ```
|
|
527
550
|
*/
|
|
528
551
|
var Replane = class {
|
|
552
|
+
constructor(options = {}) {
|
|
553
|
+
asReplaneHandle(this)._replane = new ReplaneImpl(options);
|
|
554
|
+
}
|
|
555
|
+
connect(options) {
|
|
556
|
+
return asReplaneHandle(this)._replane.connect(options);
|
|
557
|
+
}
|
|
558
|
+
disconnect() {
|
|
559
|
+
asReplaneHandle(this)._replane.disconnect();
|
|
560
|
+
}
|
|
561
|
+
get(configName, options) {
|
|
562
|
+
return asReplaneHandle(this)._replane.get(configName, options);
|
|
563
|
+
}
|
|
564
|
+
subscribe(configName, callback) {
|
|
565
|
+
return asReplaneHandle(this)._replane.subscribe(configName, callback);
|
|
566
|
+
}
|
|
567
|
+
getSnapshot() {
|
|
568
|
+
return asReplaneHandle(this)._replane.getSnapshot();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
var ReplaneImpl = class {
|
|
529
572
|
configs;
|
|
530
573
|
context;
|
|
531
574
|
logger;
|
|
532
575
|
storage = null;
|
|
533
576
|
configSubscriptions = new Map();
|
|
534
|
-
clientSubscriptions = new Set();
|
|
535
577
|
/**
|
|
536
578
|
* Create a new Replane client.
|
|
537
579
|
*
|
|
@@ -542,7 +584,11 @@ var Replane = class {
|
|
|
542
584
|
*/
|
|
543
585
|
constructor(options = {}) {
|
|
544
586
|
this.logger = options.logger ?? console;
|
|
545
|
-
|
|
587
|
+
const autoGeneratedContext = { [REPLANE_CLIENT_ID_KEY]: generateClientId() };
|
|
588
|
+
this.context = {
|
|
589
|
+
...autoGeneratedContext,
|
|
590
|
+
...options.context ?? {}
|
|
591
|
+
};
|
|
546
592
|
const initialConfigs = [];
|
|
547
593
|
if (options.snapshot) for (const config of options.snapshot.configs) initialConfigs.push({
|
|
548
594
|
name: config.name,
|
|
@@ -647,29 +693,28 @@ var Replane = class {
|
|
|
647
693
|
return config.value;
|
|
648
694
|
}
|
|
649
695
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Subscribe to a specific config's changes.
|
|
698
|
+
*
|
|
699
|
+
* @param configName - The config to watch
|
|
700
|
+
* @param callback - Function called when the config changes
|
|
701
|
+
* @returns Unsubscribe function
|
|
702
|
+
*
|
|
703
|
+
* @example
|
|
704
|
+
* ```typescript
|
|
705
|
+
* const unsubscribe = client.subscribe('myConfig', (change) => {
|
|
706
|
+
* console.log(`myConfig changed to ${change.value}`);
|
|
707
|
+
* });
|
|
708
|
+
* ```
|
|
709
|
+
*/
|
|
710
|
+
subscribe(configName, callback) {
|
|
711
|
+
const wrappedCallback = (config) => {
|
|
712
|
+
callback(config);
|
|
662
713
|
};
|
|
663
|
-
if (configName === void 0) {
|
|
664
|
-
this.clientSubscriptions.add(callback);
|
|
665
|
-
return () => {
|
|
666
|
-
this.clientSubscriptions.delete(callback);
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
714
|
if (!this.configSubscriptions.has(configName)) this.configSubscriptions.set(configName, new Set());
|
|
670
|
-
this.configSubscriptions.get(configName).add(
|
|
715
|
+
this.configSubscriptions.get(configName).add(wrappedCallback);
|
|
671
716
|
return () => {
|
|
672
|
-
this.configSubscriptions.get(configName)?.delete(
|
|
717
|
+
this.configSubscriptions.get(configName)?.delete(wrappedCallback);
|
|
673
718
|
if (this.configSubscriptions.get(configName)?.size === 0) this.configSubscriptions.delete(configName);
|
|
674
719
|
};
|
|
675
720
|
}
|
|
@@ -724,8 +769,13 @@ var Replane = class {
|
|
|
724
769
|
})
|
|
725
770
|
});
|
|
726
771
|
for await (const event of replicationStream) {
|
|
727
|
-
|
|
728
|
-
|
|
772
|
+
if (event.type === "init") {
|
|
773
|
+
this.processConfigUpdates(event.configs);
|
|
774
|
+
this.logger.info(`Replane: initialized with ${event.configs.length} config(s)`);
|
|
775
|
+
} else {
|
|
776
|
+
this.processConfigUpdates([event.config]);
|
|
777
|
+
this.logger.info(`Replane: config "${event.config.name}" updated`);
|
|
778
|
+
}
|
|
729
779
|
clientReady.resolve();
|
|
730
780
|
}
|
|
731
781
|
} catch (error) {
|
|
@@ -745,7 +795,6 @@ var Replane = class {
|
|
|
745
795
|
name: config.name,
|
|
746
796
|
value: config.value
|
|
747
797
|
};
|
|
748
|
-
for (const callback of this.clientSubscriptions) callback(change);
|
|
749
798
|
for (const callback of this.configSubscriptions.get(config.name) ?? []) callback(change);
|
|
750
799
|
}
|
|
751
800
|
}
|
|
@@ -834,6 +883,250 @@ async function getReplaneSnapshot(options) {
|
|
|
834
883
|
}
|
|
835
884
|
|
|
836
885
|
//#endregion
|
|
886
|
+
//#region src/in-memory.ts
|
|
887
|
+
function asHandle(client) {
|
|
888
|
+
return client;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* An in-memory Replane client for testing.
|
|
892
|
+
*
|
|
893
|
+
* This client provides the same interface as `Replane` but stores
|
|
894
|
+
* all configs in memory. It's useful for unit tests where you don't want
|
|
895
|
+
* to connect to a real Replane server.
|
|
896
|
+
*
|
|
897
|
+
* @example
|
|
898
|
+
* ```typescript
|
|
899
|
+
* // Basic usage
|
|
900
|
+
* const client = new InMemoryReplaneClient({
|
|
901
|
+
* defaults: { "feature-enabled": true, "rate-limit": 100 },
|
|
902
|
+
* });
|
|
903
|
+
* expect(client.get("feature-enabled")).toBe(true);
|
|
904
|
+
*
|
|
905
|
+
* // Update config at runtime
|
|
906
|
+
* client.set("feature-enabled", false);
|
|
907
|
+
* expect(client.get("feature-enabled")).toBe(false);
|
|
908
|
+
*
|
|
909
|
+
* // With overrides
|
|
910
|
+
* client.setConfig("rate-limit", 100, {
|
|
911
|
+
* overrides: [{
|
|
912
|
+
* name: "premium-users",
|
|
913
|
+
* conditions: [{ operator: "equals", property: "plan", value: "premium" }],
|
|
914
|
+
* value: 1000,
|
|
915
|
+
* }],
|
|
916
|
+
* });
|
|
917
|
+
* expect(client.get("rate-limit")).toBe(100);
|
|
918
|
+
* expect(client.get("rate-limit", { context: { plan: "premium" } })).toBe(1000);
|
|
919
|
+
* ```
|
|
920
|
+
*
|
|
921
|
+
* @typeParam T - Type definition for config keys and values
|
|
922
|
+
*/
|
|
923
|
+
var InMemoryReplaneClient = class {
|
|
924
|
+
constructor(options = {}) {
|
|
925
|
+
asHandle(this)._impl = new InMemoryReplaneClientImpl(options);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Get a config value by name.
|
|
929
|
+
*
|
|
930
|
+
* Evaluates any overrides based on the client context and per-call context.
|
|
931
|
+
*
|
|
932
|
+
* @param configName - The name of the config to retrieve
|
|
933
|
+
* @param options - Optional settings for this call
|
|
934
|
+
* @returns The config value
|
|
935
|
+
* @throws {ReplaneError} If config not found and no default provided
|
|
936
|
+
*/
|
|
937
|
+
get(configName, options) {
|
|
938
|
+
return asHandle(this)._impl.get(configName, options);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Subscribe to a specific config's changes.
|
|
942
|
+
*
|
|
943
|
+
* @param configName - The config to watch
|
|
944
|
+
* @param callback - Function called when the config changes
|
|
945
|
+
* @returns Unsubscribe function
|
|
946
|
+
*/
|
|
947
|
+
subscribe(configName, callback) {
|
|
948
|
+
return asHandle(this)._impl.subscribe(configName, callback);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Get a serializable snapshot of the current client state.
|
|
952
|
+
*
|
|
953
|
+
* @returns Snapshot object that can be serialized to JSON
|
|
954
|
+
*/
|
|
955
|
+
getSnapshot() {
|
|
956
|
+
return asHandle(this)._impl.getSnapshot();
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Set a config value (simple form without overrides).
|
|
960
|
+
*
|
|
961
|
+
* @param name - Config name
|
|
962
|
+
* @param value - Config value
|
|
963
|
+
*
|
|
964
|
+
* @example
|
|
965
|
+
* ```typescript
|
|
966
|
+
* client.set("feature-enabled", true);
|
|
967
|
+
* client.set("rate-limit", 500);
|
|
968
|
+
* ```
|
|
969
|
+
*/
|
|
970
|
+
set(name, value) {
|
|
971
|
+
asHandle(this)._impl.set(name, value);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Set a config with optional overrides.
|
|
975
|
+
*
|
|
976
|
+
* @param name - Config name
|
|
977
|
+
* @param value - Base config value
|
|
978
|
+
* @param options - Optional settings including overrides
|
|
979
|
+
*
|
|
980
|
+
* @example
|
|
981
|
+
* ```typescript
|
|
982
|
+
* client.setConfig("rate-limit", 100, {
|
|
983
|
+
* overrides: [{
|
|
984
|
+
* name: "premium-users",
|
|
985
|
+
* conditions: [
|
|
986
|
+
* { operator: "in", property: "plan", value: ["pro", "enterprise"] }
|
|
987
|
+
* ],
|
|
988
|
+
* value: 1000,
|
|
989
|
+
* }],
|
|
990
|
+
* });
|
|
991
|
+
* ```
|
|
992
|
+
*/
|
|
993
|
+
setConfig(name, value, options) {
|
|
994
|
+
asHandle(this)._impl.setConfig(name, value, options);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Delete a config.
|
|
998
|
+
*
|
|
999
|
+
* @param name - Config name to delete
|
|
1000
|
+
* @returns True if config was deleted, false if it didn't exist
|
|
1001
|
+
*/
|
|
1002
|
+
delete(name) {
|
|
1003
|
+
return asHandle(this)._impl.delete(name);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Clear all configs.
|
|
1007
|
+
*/
|
|
1008
|
+
clear() {
|
|
1009
|
+
asHandle(this)._impl.clear();
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if a config exists.
|
|
1013
|
+
*
|
|
1014
|
+
* @param name - Config name to check
|
|
1015
|
+
* @returns True if config exists
|
|
1016
|
+
*/
|
|
1017
|
+
has(name) {
|
|
1018
|
+
return asHandle(this)._impl.has(name);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Get all config names.
|
|
1022
|
+
*
|
|
1023
|
+
* @returns Array of config names
|
|
1024
|
+
*/
|
|
1025
|
+
keys() {
|
|
1026
|
+
return asHandle(this)._impl.keys();
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
var InMemoryReplaneClientImpl = class {
|
|
1030
|
+
configs;
|
|
1031
|
+
context;
|
|
1032
|
+
logger;
|
|
1033
|
+
configSubscriptions = new Map();
|
|
1034
|
+
constructor(options = {}) {
|
|
1035
|
+
this.logger = options.logger ?? console;
|
|
1036
|
+
this.context = options.context ?? {};
|
|
1037
|
+
const initialConfigs = [];
|
|
1038
|
+
if (options.defaults) {
|
|
1039
|
+
for (const [name, value] of Object.entries(options.defaults)) if (value !== void 0) initialConfigs.push({
|
|
1040
|
+
name,
|
|
1041
|
+
value,
|
|
1042
|
+
overrides: []
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
this.configs = new Map(initialConfigs.map((config) => [config.name, config]));
|
|
1046
|
+
}
|
|
1047
|
+
get(configName, options = {}) {
|
|
1048
|
+
const config = this.configs.get(String(configName));
|
|
1049
|
+
if (config === void 0) {
|
|
1050
|
+
if ("default" in options) return options.default;
|
|
1051
|
+
throw new ReplaneError({
|
|
1052
|
+
message: `Config not found: ${String(configName)}`,
|
|
1053
|
+
code: ReplaneErrorCode.NotFound
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
return evaluateOverrides(config.value, config.overrides, {
|
|
1058
|
+
...this.context,
|
|
1059
|
+
...options?.context ?? {}
|
|
1060
|
+
}, this.logger);
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
this.logger.error(`Replane: error evaluating overrides for config ${String(configName)}:`, error);
|
|
1063
|
+
return config.value;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
subscribe(configName, callback) {
|
|
1067
|
+
const wrappedCallback = (config) => {
|
|
1068
|
+
callback(config);
|
|
1069
|
+
};
|
|
1070
|
+
if (!this.configSubscriptions.has(configName)) this.configSubscriptions.set(configName, new Set());
|
|
1071
|
+
this.configSubscriptions.get(configName).add(wrappedCallback);
|
|
1072
|
+
return () => {
|
|
1073
|
+
this.configSubscriptions.get(configName)?.delete(wrappedCallback);
|
|
1074
|
+
if (this.configSubscriptions.get(configName)?.size === 0) this.configSubscriptions.delete(configName);
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
getSnapshot() {
|
|
1078
|
+
return { configs: [...this.configs.values()].map((config) => ({
|
|
1079
|
+
name: config.name,
|
|
1080
|
+
value: config.value,
|
|
1081
|
+
overrides: config.overrides.map((override) => ({
|
|
1082
|
+
name: override.name,
|
|
1083
|
+
conditions: override.conditions,
|
|
1084
|
+
value: override.value
|
|
1085
|
+
}))
|
|
1086
|
+
})) };
|
|
1087
|
+
}
|
|
1088
|
+
set(name, value) {
|
|
1089
|
+
this.setConfig(name, value);
|
|
1090
|
+
}
|
|
1091
|
+
setConfig(name, value, options) {
|
|
1092
|
+
const overrides = options?.overrides ?? [];
|
|
1093
|
+
const config = {
|
|
1094
|
+
name: String(name),
|
|
1095
|
+
value,
|
|
1096
|
+
overrides
|
|
1097
|
+
};
|
|
1098
|
+
this.configs.set(String(name), config);
|
|
1099
|
+
this.notifySubscribers(name, value);
|
|
1100
|
+
}
|
|
1101
|
+
delete(name) {
|
|
1102
|
+
const existed = this.configs.has(String(name));
|
|
1103
|
+
this.configs.delete(String(name));
|
|
1104
|
+
return existed;
|
|
1105
|
+
}
|
|
1106
|
+
clear() {
|
|
1107
|
+
this.configs.clear();
|
|
1108
|
+
}
|
|
1109
|
+
has(name) {
|
|
1110
|
+
return this.configs.has(String(name));
|
|
1111
|
+
}
|
|
1112
|
+
keys() {
|
|
1113
|
+
return [...this.configs.keys()];
|
|
1114
|
+
}
|
|
1115
|
+
notifySubscribers(name, value) {
|
|
1116
|
+
const change = {
|
|
1117
|
+
name,
|
|
1118
|
+
value
|
|
1119
|
+
};
|
|
1120
|
+
for (const callback of this.configSubscriptions.get(name) ?? []) try {
|
|
1121
|
+
callback(change);
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
this.logger.error(`Replane: error in subscription callback for ${String(name)}:`, error);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
//#endregion
|
|
1129
|
+
exports.InMemoryReplaneClient = InMemoryReplaneClient;
|
|
837
1130
|
exports.Replane = Replane;
|
|
838
1131
|
exports.ReplaneError = ReplaneError;
|
|
839
1132
|
exports.ReplaneErrorCode = ReplaneErrorCode;
|