@replanejs/sdk 0.8.20 → 0.9.2
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 +78 -83
- package/dist/index.cjs +277 -264
- package/dist/index.d.cts +197 -186
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +197 -186
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +276 -261
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -230,13 +230,13 @@ const SUPPORTED_REPLICATION_STREAM_RECORD_TYPES = Object.keys({
|
|
|
230
230
|
* and streams config updates via SSE.
|
|
231
231
|
*/
|
|
232
232
|
var ReplaneRemoteStorage = class {
|
|
233
|
-
|
|
233
|
+
disconnectController = new AbortController();
|
|
234
234
|
/**
|
|
235
235
|
* Start a replication stream that yields config updates.
|
|
236
236
|
* This method never throws - it retries on failure with exponential backoff.
|
|
237
237
|
*/
|
|
238
238
|
async *startReplicationStream(options) {
|
|
239
|
-
const { signal, cleanUpSignals } = combineAbortSignals([this.
|
|
239
|
+
const { signal, cleanUpSignals } = combineAbortSignals([this.disconnectController.signal, options.signal]);
|
|
240
240
|
try {
|
|
241
241
|
let failedAttempts = 0;
|
|
242
242
|
while (!signal.aborted) try {
|
|
@@ -252,7 +252,6 @@ var ReplaneRemoteStorage = class {
|
|
|
252
252
|
const retryDelayMs = Math.min(options.retryDelayMs * 2 ** (failedAttempts - 1), 1e4);
|
|
253
253
|
if (!signal.aborted) {
|
|
254
254
|
options.logger.error(`Failed to fetch project events, retrying in ${retryDelayMs}ms...`, error);
|
|
255
|
-
options.onConnectionError?.(error);
|
|
256
255
|
await retryDelay(retryDelayMs);
|
|
257
256
|
}
|
|
258
257
|
}
|
|
@@ -289,7 +288,6 @@ var ReplaneRemoteStorage = class {
|
|
|
289
288
|
onConnect: () => {
|
|
290
289
|
resetInactivityTimer();
|
|
291
290
|
options.onConnect?.();
|
|
292
|
-
options.onConnected?.();
|
|
293
291
|
}
|
|
294
292
|
});
|
|
295
293
|
for await (const sseEvent of rawEvents) {
|
|
@@ -304,10 +302,11 @@ var ReplaneRemoteStorage = class {
|
|
|
304
302
|
}
|
|
305
303
|
}
|
|
306
304
|
/**
|
|
307
|
-
*
|
|
305
|
+
* Disconnect the storage and abort any active connections
|
|
308
306
|
*/
|
|
309
|
-
|
|
310
|
-
this.
|
|
307
|
+
disconnect() {
|
|
308
|
+
this.disconnectController.abort();
|
|
309
|
+
this.disconnectController = new AbortController();
|
|
311
310
|
}
|
|
312
311
|
getAuthHeader(options) {
|
|
313
312
|
return `Bearer ${options.sdkKey}`;
|
|
@@ -483,66 +482,156 @@ function castToContextType(expectedValue, contextValue) {
|
|
|
483
482
|
|
|
484
483
|
//#endregion
|
|
485
484
|
//#region src/version.ts
|
|
486
|
-
const VERSION = "0.
|
|
485
|
+
const VERSION = "0.9.2";
|
|
487
486
|
const DEFAULT_AGENT = `replane-js-sdk/${VERSION}`;
|
|
488
487
|
|
|
489
488
|
//#endregion
|
|
490
489
|
//#region src/client.ts
|
|
491
490
|
/**
|
|
492
|
-
*
|
|
491
|
+
* The Replane client for managing dynamic configuration.
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```typescript
|
|
495
|
+
* // Create client with defaults
|
|
496
|
+
* const client = new Replane({
|
|
497
|
+
* defaults: { myConfig: 'defaultValue' }
|
|
498
|
+
* });
|
|
499
|
+
*
|
|
500
|
+
* // Use immediately (returns defaults)
|
|
501
|
+
* const value = client.get('myConfig');
|
|
502
|
+
*
|
|
503
|
+
* // Connect for real-time updates
|
|
504
|
+
* await client.connect({
|
|
505
|
+
* baseUrl: 'https://app.replane.dev',
|
|
506
|
+
* sdkKey: 'your-sdk-key'
|
|
507
|
+
* });
|
|
508
|
+
* ```
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* ```typescript
|
|
512
|
+
* // SSR/Hydration: Create from snapshot
|
|
513
|
+
* const client = new Replane({
|
|
514
|
+
* snapshot: serverSnapshot
|
|
515
|
+
* });
|
|
516
|
+
* await client.connect({ baseUrl, sdkKey });
|
|
517
|
+
* ```
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```typescript
|
|
521
|
+
* // In-memory mode (no connection)
|
|
522
|
+
* const client = new Replane({
|
|
523
|
+
* defaults: { feature: true, limit: 100 }
|
|
524
|
+
* });
|
|
525
|
+
* // Don't call connect() - works entirely in-memory
|
|
526
|
+
* ```
|
|
493
527
|
*/
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
528
|
+
var Replane = class {
|
|
529
|
+
configs;
|
|
530
|
+
context;
|
|
531
|
+
logger;
|
|
532
|
+
storage = null;
|
|
533
|
+
configSubscriptions = new Map();
|
|
534
|
+
clientSubscriptions = new Set();
|
|
535
|
+
/**
|
|
536
|
+
* Create a new Replane client.
|
|
537
|
+
*
|
|
538
|
+
* The client is usable immediately after construction with defaults or snapshot data.
|
|
539
|
+
* Call `connect()` to establish a real-time connection for live updates.
|
|
540
|
+
*
|
|
541
|
+
* @param options - Configuration options
|
|
542
|
+
*/
|
|
543
|
+
constructor(options = {}) {
|
|
544
|
+
this.logger = options.logger ?? console;
|
|
545
|
+
this.context = { ...options.context ?? {} };
|
|
546
|
+
const initialConfigs = [];
|
|
547
|
+
if (options.snapshot) for (const config of options.snapshot.configs) initialConfigs.push({
|
|
548
|
+
name: config.name,
|
|
549
|
+
value: config.value,
|
|
550
|
+
overrides: config.overrides
|
|
551
|
+
});
|
|
552
|
+
if (options.defaults) {
|
|
553
|
+
const snapshotNames = new Set(initialConfigs.map((c) => c.name));
|
|
554
|
+
for (const [name, value] of Object.entries(options.defaults)) if (value !== void 0 && !snapshotNames.has(name)) initialConfigs.push({
|
|
555
|
+
name,
|
|
556
|
+
value,
|
|
557
|
+
overrides: []
|
|
514
558
|
});
|
|
515
559
|
}
|
|
560
|
+
this.configs = new Map(initialConfigs.map((config) => [config.name, config]));
|
|
516
561
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Connect to the Replane server for real-time config updates.
|
|
564
|
+
*
|
|
565
|
+
* This method establishes an SSE connection to receive live config updates.
|
|
566
|
+
* If already connected, it will disconnect first and reconnect with new options.
|
|
567
|
+
*
|
|
568
|
+
* @param options - Connection options including baseUrl and sdkKey
|
|
569
|
+
* @returns Promise that resolves when the initial connection is established
|
|
570
|
+
* @throws {ReplaneError} If connection times out and no defaults are available
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* ```typescript
|
|
574
|
+
* await client.connect({
|
|
575
|
+
* baseUrl: 'https://app.replane.dev',
|
|
576
|
+
* sdkKey: 'rp_xxx'
|
|
577
|
+
* });
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
async connect(options) {
|
|
581
|
+
this.disconnect();
|
|
582
|
+
const finalOptions = this.toFinalOptions(options);
|
|
583
|
+
this.storage = new ReplaneRemoteStorage();
|
|
584
|
+
const clientReady = new Deferred();
|
|
585
|
+
this.startStreaming(finalOptions, clientReady);
|
|
586
|
+
const timeoutId = setTimeout(() => {
|
|
587
|
+
this.disconnect();
|
|
588
|
+
clientReady.reject(new ReplaneError({
|
|
589
|
+
message: "Replane client connection timed out",
|
|
590
|
+
code: ReplaneErrorCode.Timeout
|
|
591
|
+
}));
|
|
592
|
+
}, finalOptions.connectTimeoutMs);
|
|
593
|
+
clientReady.promise.finally(() => clearTimeout(timeoutId));
|
|
594
|
+
await clientReady.promise;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Disconnect from the Replane server.
|
|
598
|
+
*
|
|
599
|
+
* Stops the SSE connection and cleans up resources.
|
|
600
|
+
* The client remains usable with cached config values.
|
|
601
|
+
* Can call `connect()` again to reconnect.
|
|
602
|
+
*/
|
|
603
|
+
disconnect() {
|
|
604
|
+
if (this.storage) {
|
|
605
|
+
this.storage.disconnect();
|
|
606
|
+
this.storage = null;
|
|
540
607
|
}
|
|
541
608
|
}
|
|
542
|
-
|
|
543
|
-
|
|
609
|
+
/**
|
|
610
|
+
* Get a config value by name.
|
|
611
|
+
*
|
|
612
|
+
* Evaluates any overrides based on the client context and per-call context.
|
|
613
|
+
*
|
|
614
|
+
* @param configName - The name of the config to retrieve
|
|
615
|
+
* @param options - Optional settings for this call
|
|
616
|
+
* @returns The config value
|
|
617
|
+
* @throws {ReplaneError} If config not found and no default provided
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* ```typescript
|
|
621
|
+
* // Simple get
|
|
622
|
+
* const value = client.get('myConfig');
|
|
623
|
+
*
|
|
624
|
+
* // With default fallback
|
|
625
|
+
* const value = client.get('myConfig', { default: 'fallback' });
|
|
626
|
+
*
|
|
627
|
+
* // With per-call context for override evaluation
|
|
628
|
+
* const value = client.get('myConfig', { context: { userId: '123' } });
|
|
629
|
+
* ```
|
|
630
|
+
*/
|
|
631
|
+
get(configName, options = {}) {
|
|
632
|
+
const config = this.configs.get(String(configName));
|
|
544
633
|
if (config === void 0) {
|
|
545
|
-
if ("default" in
|
|
634
|
+
if ("default" in options) return options.default;
|
|
546
635
|
throw new ReplaneError({
|
|
547
636
|
message: `Config not found: ${String(configName)}`,
|
|
548
637
|
code: ReplaneErrorCode.NotFound
|
|
@@ -550,15 +639,15 @@ function createClientCore(options) {
|
|
|
550
639
|
}
|
|
551
640
|
try {
|
|
552
641
|
return evaluateOverrides(config.value, config.overrides, {
|
|
553
|
-
...context,
|
|
554
|
-
...
|
|
555
|
-
}, logger);
|
|
642
|
+
...this.context,
|
|
643
|
+
...options?.context ?? {}
|
|
644
|
+
}, this.logger);
|
|
556
645
|
} catch (error) {
|
|
557
|
-
logger.error(`Replane: error evaluating overrides for config ${String(configName)}:`, error);
|
|
646
|
+
this.logger.error(`Replane: error evaluating overrides for config ${String(configName)}:`, error);
|
|
558
647
|
return config.value;
|
|
559
648
|
}
|
|
560
649
|
}
|
|
561
|
-
|
|
650
|
+
subscribe(callbackOrConfigName, callbackOrUndefined) {
|
|
562
651
|
let configName = void 0;
|
|
563
652
|
let callback;
|
|
564
653
|
if (typeof callbackOrConfigName === "function") callback = callbackOrConfigName;
|
|
@@ -572,20 +661,38 @@ function createClientCore(options) {
|
|
|
572
661
|
originalCallback(...args);
|
|
573
662
|
};
|
|
574
663
|
if (configName === void 0) {
|
|
575
|
-
clientSubscriptions.add(callback);
|
|
664
|
+
this.clientSubscriptions.add(callback);
|
|
576
665
|
return () => {
|
|
577
|
-
clientSubscriptions.delete(callback);
|
|
666
|
+
this.clientSubscriptions.delete(callback);
|
|
578
667
|
};
|
|
579
668
|
}
|
|
580
|
-
if (!configSubscriptions.has(configName)) configSubscriptions.set(configName, new Set());
|
|
581
|
-
configSubscriptions.get(configName).add(callback);
|
|
669
|
+
if (!this.configSubscriptions.has(configName)) this.configSubscriptions.set(configName, new Set());
|
|
670
|
+
this.configSubscriptions.get(configName).add(callback);
|
|
582
671
|
return () => {
|
|
583
|
-
configSubscriptions.get(configName)?.delete(callback);
|
|
584
|
-
if (configSubscriptions.get(configName)?.size === 0) configSubscriptions.delete(configName);
|
|
672
|
+
this.configSubscriptions.get(configName)?.delete(callback);
|
|
673
|
+
if (this.configSubscriptions.get(configName)?.size === 0) this.configSubscriptions.delete(configName);
|
|
585
674
|
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Get a serializable snapshot of the current client state.
|
|
678
|
+
*
|
|
679
|
+
* Useful for SSR/hydration scenarios where you want to pass
|
|
680
|
+
* configs from server to client.
|
|
681
|
+
*
|
|
682
|
+
* @returns Snapshot object that can be serialized to JSON
|
|
683
|
+
*
|
|
684
|
+
* @example
|
|
685
|
+
* ```typescript
|
|
686
|
+
* // On server
|
|
687
|
+
* const snapshot = client.getSnapshot();
|
|
688
|
+
* const json = JSON.stringify(snapshot);
|
|
689
|
+
*
|
|
690
|
+
* // On client
|
|
691
|
+
* const client = new Replane({ snapshot: JSON.parse(json) });
|
|
692
|
+
* ```
|
|
693
|
+
*/
|
|
694
|
+
getSnapshot() {
|
|
695
|
+
return { configs: [...this.configs.values()].map((config) => ({
|
|
589
696
|
name: config.name,
|
|
590
697
|
value: config.value,
|
|
591
698
|
overrides: config.overrides.map((override) => ({
|
|
@@ -593,196 +700,83 @@ function createClientCore(options) {
|
|
|
593
700
|
conditions: override.conditions,
|
|
594
701
|
value: override.value
|
|
595
702
|
}))
|
|
596
|
-
}))
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
* ```
|
|
624
|
-
*/
|
|
625
|
-
async function createReplaneClient(sdkOptions) {
|
|
626
|
-
const storage = new ReplaneRemoteStorage();
|
|
627
|
-
return await createReplaneClientInternal(toFinalOptions(sdkOptions), storage);
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Create a Replane client that uses in-memory storage.
|
|
631
|
-
* Useful for testing or when you have static config values.
|
|
632
|
-
*
|
|
633
|
-
* @example
|
|
634
|
-
* ```typescript
|
|
635
|
-
* const client = createInMemoryReplaneClient({ 'my-config': 123 });
|
|
636
|
-
* const value = client.get('my-config'); // 123
|
|
637
|
-
* ```
|
|
638
|
-
*/
|
|
639
|
-
function createInMemoryReplaneClient(initialData) {
|
|
640
|
-
return {
|
|
641
|
-
get: (configName, options) => {
|
|
642
|
-
const config = initialData[configName];
|
|
643
|
-
if (config === void 0) {
|
|
644
|
-
if (options && "default" in options) return options.default;
|
|
645
|
-
throw new ReplaneError({
|
|
646
|
-
message: `Config not found: ${String(configName)}`,
|
|
647
|
-
code: ReplaneErrorCode.NotFound
|
|
648
|
-
});
|
|
703
|
+
})) };
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Check if the client is currently connected.
|
|
707
|
+
*/
|
|
708
|
+
get isConnected() {
|
|
709
|
+
return this.storage !== null;
|
|
710
|
+
}
|
|
711
|
+
async startStreaming(options, clientReady) {
|
|
712
|
+
if (!this.storage) return;
|
|
713
|
+
try {
|
|
714
|
+
const replicationStream = this.storage.startReplicationStream({
|
|
715
|
+
...options,
|
|
716
|
+
logger: this.logger,
|
|
717
|
+
getBody: () => ({
|
|
718
|
+
currentConfigs: [...this.configs.values()].map((config) => ({
|
|
719
|
+
name: config.name,
|
|
720
|
+
overrides: config.overrides,
|
|
721
|
+
value: config.value
|
|
722
|
+
})),
|
|
723
|
+
requiredConfigs: []
|
|
724
|
+
})
|
|
725
|
+
});
|
|
726
|
+
for await (const event of replicationStream) {
|
|
727
|
+
const updatedConfigs = event.type === "config_change" ? [event.config] : event.configs;
|
|
728
|
+
this.processConfigUpdates(updatedConfigs);
|
|
729
|
+
clientReady.resolve();
|
|
649
730
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
},
|
|
655
|
-
getSnapshot: () => ({ configs: Object.entries(initialData).map(([name, value]) => ({
|
|
656
|
-
name,
|
|
657
|
-
value,
|
|
658
|
-
overrides: []
|
|
659
|
-
})) }),
|
|
660
|
-
close: () => {}
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Restore a Replane client from a snapshot.
|
|
665
|
-
* This is useful for SSR/hydration scenarios where the server has already fetched configs.
|
|
666
|
-
*
|
|
667
|
-
* @example
|
|
668
|
-
* ```typescript
|
|
669
|
-
* // On the server:
|
|
670
|
-
* const serverClient = await createReplaneClient({ ... });
|
|
671
|
-
* const snapshot = serverClient.getSnapshot();
|
|
672
|
-
* // Pass snapshot to client via props/serialization
|
|
673
|
-
*
|
|
674
|
-
* // On the client:
|
|
675
|
-
* const client = restoreReplaneClient({
|
|
676
|
-
* snapshot,
|
|
677
|
-
* connection: { sdkKey, baseUrl }
|
|
678
|
-
* });
|
|
679
|
-
* const value = client.get('my-config');
|
|
680
|
-
* ```
|
|
681
|
-
*/
|
|
682
|
-
function restoreReplaneClient(options) {
|
|
683
|
-
const { snapshot, connection } = options;
|
|
684
|
-
const context = options.context ?? snapshot.context ?? {};
|
|
685
|
-
const logger = connection?.logger ?? console;
|
|
686
|
-
const initialConfigs = snapshot.configs.map((config) => ({
|
|
687
|
-
name: config.name,
|
|
688
|
-
value: config.value,
|
|
689
|
-
overrides: config.overrides
|
|
690
|
-
}));
|
|
691
|
-
let storage = null;
|
|
692
|
-
let streamOptions = null;
|
|
693
|
-
if (connection) {
|
|
694
|
-
storage = new ReplaneRemoteStorage();
|
|
695
|
-
streamOptions = toFinalOptions(connection);
|
|
696
|
-
}
|
|
697
|
-
const { client, startStreaming } = createClientCore({
|
|
698
|
-
initialConfigs,
|
|
699
|
-
context,
|
|
700
|
-
logger,
|
|
701
|
-
storage,
|
|
702
|
-
streamOptions,
|
|
703
|
-
requiredConfigs: []
|
|
704
|
-
});
|
|
705
|
-
if (storage && streamOptions) startStreaming().catch((error) => {
|
|
706
|
-
logger.error("Replane: error in restored client SSE connection:", error);
|
|
707
|
-
});
|
|
708
|
-
return client;
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Internal function to create a Replane client with the given options and storage
|
|
712
|
-
*/
|
|
713
|
-
async function createReplaneClientInternal(sdkOptions, storage) {
|
|
714
|
-
const { client, configs, startStreaming, clientReady } = createClientCore({
|
|
715
|
-
initialConfigs: sdkOptions.defaults,
|
|
716
|
-
context: sdkOptions.context,
|
|
717
|
-
logger: sdkOptions.logger,
|
|
718
|
-
storage,
|
|
719
|
-
streamOptions: sdkOptions,
|
|
720
|
-
requiredConfigs: sdkOptions.requiredConfigs
|
|
721
|
-
});
|
|
722
|
-
startStreaming().catch((error) => {
|
|
723
|
-
sdkOptions.logger.error("Replane: error initializing client:", error);
|
|
724
|
-
});
|
|
725
|
-
const initializationTimeoutId = setTimeout(() => {
|
|
726
|
-
if (sdkOptions.defaults.length === 0) {
|
|
727
|
-
client.close();
|
|
728
|
-
clientReady.reject(new ReplaneError({
|
|
729
|
-
message: "Replane client initialization timed out",
|
|
730
|
-
code: ReplaneErrorCode.Timeout
|
|
731
|
-
}));
|
|
732
|
-
return;
|
|
731
|
+
} catch (error) {
|
|
732
|
+
this.logger.error("Replane: error in SSE connection:", error);
|
|
733
|
+
clientReady.reject(error);
|
|
734
|
+
throw error;
|
|
733
735
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
})
|
|
742
|
-
|
|
736
|
+
}
|
|
737
|
+
processConfigUpdates(updatedConfigs) {
|
|
738
|
+
for (const config of updatedConfigs) {
|
|
739
|
+
this.configs.set(config.name, {
|
|
740
|
+
name: config.name,
|
|
741
|
+
overrides: config.overrides,
|
|
742
|
+
value: config.value
|
|
743
|
+
});
|
|
744
|
+
const change = {
|
|
745
|
+
name: config.name,
|
|
746
|
+
value: config.value
|
|
747
|
+
};
|
|
748
|
+
for (const callback of this.clientSubscriptions) callback(change);
|
|
749
|
+
for (const callback of this.configSubscriptions.get(config.name) ?? []) callback(change);
|
|
743
750
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
requestTimeoutMs: options.requestTimeoutMs ?? 2e3,
|
|
759
|
-
initializationTimeoutMs: options.initializationTimeoutMs ?? 5e3,
|
|
760
|
-
inactivityTimeoutMs: options.inactivityTimeoutMs ?? 3e4,
|
|
761
|
-
logger: options.logger ?? console,
|
|
762
|
-
retryDelayMs: options.retryDelayMs ?? 200,
|
|
763
|
-
context: { ...options.context ?? {} },
|
|
764
|
-
requiredConfigs: Array.isArray(options.required) ? options.required.map((name) => String(name)) : Object.entries(options.required ?? {}).filter(([_, value]) => value !== void 0).map(([name]) => name),
|
|
765
|
-
defaults: Object.entries(options.defaults ?? {}).filter(([_, value]) => value !== void 0).map(([name, value]) => ({
|
|
766
|
-
name,
|
|
767
|
-
overrides: [],
|
|
768
|
-
version: -1,
|
|
769
|
-
value
|
|
770
|
-
})),
|
|
771
|
-
agent: options.agent ?? DEFAULT_AGENT,
|
|
772
|
-
onConnectionError: options.onConnectionError,
|
|
773
|
-
onConnected: options.onConnected
|
|
774
|
-
};
|
|
775
|
-
}
|
|
751
|
+
}
|
|
752
|
+
toFinalOptions(options) {
|
|
753
|
+
return {
|
|
754
|
+
sdkKey: options.sdkKey,
|
|
755
|
+
baseUrl: (options.baseUrl ?? "").replace(/\/+$/, ""),
|
|
756
|
+
fetchFn: options.fetchFn ?? globalThis.fetch.bind(globalThis),
|
|
757
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 2e3,
|
|
758
|
+
connectTimeoutMs: options.connectTimeoutMs ?? 5e3,
|
|
759
|
+
inactivityTimeoutMs: options.inactivityTimeoutMs ?? 3e4,
|
|
760
|
+
retryDelayMs: options.retryDelayMs ?? 200,
|
|
761
|
+
agent: options.agent ?? DEFAULT_AGENT
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
};
|
|
776
765
|
|
|
777
766
|
//#endregion
|
|
778
767
|
//#region src/snapshot.ts
|
|
779
768
|
const clientCache = new Map();
|
|
769
|
+
const pendingConnections = new Map();
|
|
780
770
|
function getCacheKey(options) {
|
|
781
771
|
return `${options.baseUrl}:${options.sdkKey}`;
|
|
782
772
|
}
|
|
783
773
|
function setupCleanupTimeout(cacheKey, keepAliveMs) {
|
|
784
774
|
return setTimeout(() => {
|
|
785
|
-
clientCache.
|
|
775
|
+
const cached = clientCache.get(cacheKey);
|
|
776
|
+
if (cached) {
|
|
777
|
+
cached.client.disconnect();
|
|
778
|
+
clientCache.delete(cacheKey);
|
|
779
|
+
}
|
|
786
780
|
}, keepAliveMs);
|
|
787
781
|
}
|
|
788
782
|
/**
|
|
@@ -793,35 +787,54 @@ function setupCleanupTimeout(cacheKey, keepAliveMs) {
|
|
|
793
787
|
* @example
|
|
794
788
|
* ```ts
|
|
795
789
|
* const snapshot = await getReplaneSnapshot({
|
|
796
|
-
*
|
|
797
|
-
*
|
|
790
|
+
* connection: {
|
|
791
|
+
* baseUrl: process.env.REPLANE_BASE_URL!,
|
|
792
|
+
* sdkKey: process.env.REPLANE_SDK_KEY!,
|
|
793
|
+
* },
|
|
798
794
|
* });
|
|
799
795
|
* ```
|
|
800
796
|
*/
|
|
801
797
|
async function getReplaneSnapshot(options) {
|
|
802
|
-
const { keepAliveMs = 6e4
|
|
803
|
-
|
|
798
|
+
const { keepAliveMs = 6e4, logger, context, defaults, connection: connectionOptions } = options;
|
|
799
|
+
if (!connectionOptions) return { configs: [...Object.entries(defaults ?? {}).map(([name, value]) => ({
|
|
800
|
+
name,
|
|
801
|
+
value,
|
|
802
|
+
overrides: []
|
|
803
|
+
}))] };
|
|
804
|
+
const cacheKey = getCacheKey(connectionOptions);
|
|
804
805
|
const cached = clientCache.get(cacheKey);
|
|
805
806
|
if (cached) {
|
|
806
807
|
clearTimeout(cached.timeoutId);
|
|
807
808
|
cached.timeoutId = setupCleanupTimeout(cacheKey, keepAliveMs);
|
|
808
|
-
|
|
809
|
+
return cached.client.getSnapshot();
|
|
810
|
+
}
|
|
811
|
+
const pending = pendingConnections.get(cacheKey);
|
|
812
|
+
if (pending) {
|
|
813
|
+
const client$1 = await pending.promise;
|
|
809
814
|
return client$1.getSnapshot();
|
|
810
815
|
}
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
};
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
816
|
+
const client = new Replane({
|
|
817
|
+
logger,
|
|
818
|
+
context,
|
|
819
|
+
defaults
|
|
820
|
+
});
|
|
821
|
+
const connectionPromise = client.connect(connectionOptions).then(() => client);
|
|
822
|
+
pendingConnections.set(cacheKey, { promise: connectionPromise });
|
|
823
|
+
try {
|
|
824
|
+
await connectionPromise;
|
|
825
|
+
const entry = {
|
|
826
|
+
client,
|
|
827
|
+
timeoutId: setupCleanupTimeout(cacheKey, keepAliveMs)
|
|
828
|
+
};
|
|
829
|
+
clientCache.set(cacheKey, entry);
|
|
830
|
+
return client.getSnapshot();
|
|
831
|
+
} finally {
|
|
832
|
+
pendingConnections.delete(cacheKey);
|
|
833
|
+
}
|
|
819
834
|
}
|
|
820
835
|
|
|
821
836
|
//#endregion
|
|
837
|
+
exports.Replane = Replane;
|
|
822
838
|
exports.ReplaneError = ReplaneError;
|
|
823
839
|
exports.ReplaneErrorCode = ReplaneErrorCode;
|
|
824
|
-
exports.
|
|
825
|
-
exports.createReplaneClient = createReplaneClient;
|
|
826
|
-
exports.getReplaneSnapshot = getReplaneSnapshot;
|
|
827
|
-
exports.restoreReplaneClient = restoreReplaneClient;
|
|
840
|
+
exports.getReplaneSnapshot = getReplaneSnapshot;
|