@configdirector/nuxt-sdk 0.1.1 → 0.5.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.
@@ -380,7 +380,7 @@ var StreamingTransport = class {
380
380
  this.dispatchMessage(data);
381
381
  },
382
382
  onConnect: () => {
383
- this.logger.debug("[EventSourceTransport] Connected");
383
+ this.logger.debug("[StreamingTransport] Connected");
384
384
  resolve(this);
385
385
  },
386
386
  shouldReconnect: (state) => {
@@ -390,11 +390,13 @@ var StreamingTransport = class {
390
390
  },
391
391
  calculateReconnectDelay: (state) => {
392
392
  const delay = Math.pow(2, Math.min(state.attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
393
- this.logger.warn(`[EventSourceTransport] Scheduling reconnect in ${delay}ms.`);
393
+ const logMessage = `[StreamingTransport] Scheduling reconnect attempt #${state.attempt} in ${delay}ms.`;
394
+ if (state.attempt <= 5) this.logger.info(logMessage);
395
+ else this.logger.warn(logMessage);
394
396
  return delay;
395
397
  },
396
398
  onDisconnect: () => {
397
- this.logger.debug("[EventSourceTransport] Disconnected");
399
+ this.logger.debug("[StreamingTransport] Disconnected");
398
400
  }
399
401
  });
400
402
  this.eventSource.connect();
@@ -408,7 +410,7 @@ var StreamingTransport = class {
408
410
  const json = JSON.parse(data);
409
411
  this.eventEmitter.emit("configBundleReceived", json);
410
412
  } catch (error) {
411
- this.logger.error("[EventSourceTransport] Error parsing and dispatching config data update: ", error);
413
+ this.logger.error("[StreamingTransport] Error parsing and dispatching config data update: ", error);
412
414
  }
413
415
  }
414
416
  prepareFatalError(responseStatus, error) {
@@ -440,6 +442,148 @@ var StreamingTransport = class {
440
442
  }
441
443
  };
442
444
  //#endregion
445
+ //#region ../shared/src/transport/AbstractPollingTransport.ts
446
+ var AbstractPollingTransport = class {
447
+ fatalError = false;
448
+ pollingInterval;
449
+ pollingIntervalSeconds = 60;
450
+ clearPollingInterval() {
451
+ clearInterval(this.pollingInterval);
452
+ }
453
+ schedulePollingInterval(callback) {
454
+ clearInterval(this.pollingInterval);
455
+ if (this.pollingIntervalSeconds <= 0) return;
456
+ this.pollingInterval = setInterval(callback, this.pollingIntervalSeconds * 1e3);
457
+ }
458
+ async handleNonOkResponse(response) {
459
+ if (!response.ok) if (this.isStatusFatal(response.status)) {
460
+ this.fatalError = true;
461
+ throw this.prepareFatalResponseStatusError(response.status, await response.text());
462
+ } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
463
+ }
464
+ handleFetchError(fetchError) {
465
+ if (this.isFetchErrorFatal(fetchError)) {
466
+ this.fatalError = true;
467
+ this.close();
468
+ throw new ConfigDirectorConnectionError(`Connection failed with fatal error: ${fetchError}. This is an unrecoverable error, retry attempts will be ignored.`);
469
+ } else if (fetchError instanceof SyntaxError) throw new ConfigDirectorConnectionError(`Failed to parse the response from the server: ${fetchError}`);
470
+ else throw new ConfigDirectorConnectionError(`Connection failed with error: ${fetchError}.`);
471
+ }
472
+ prepareFatalResponseStatusError(responseStatus, errorBody) {
473
+ const status = responseStatus ?? 0;
474
+ return new ConfigDirectorConnectionError(`${`Connection failed with status: ${responseStatus ?? "unknown"}`}${(errorBody?.trim()?.length ?? 0) > 0 ? ` (${errorBody})` : ""}. This is an unrecoverable error, retry attempts will be ignored.`, status);
475
+ }
476
+ isStatusFatal(status) {
477
+ return !!status && status >= 400 && status < 500;
478
+ }
479
+ isFetchErrorFatal(fetchError) {
480
+ if (fetchError?.name === "NotAllowedError") return true;
481
+ else if (fetchError instanceof TypeError) return true;
482
+ return false;
483
+ }
484
+ get isConnected() {
485
+ return this.pollingInterval !== void 0;
486
+ }
487
+ close() {
488
+ clearInterval(this.pollingInterval);
489
+ this.pollingInterval = void 0;
490
+ }
491
+ dispose() {
492
+ this.close();
493
+ this.clear();
494
+ }
495
+ };
496
+ //#endregion
497
+ //#region ../shared/src/fetchWithTimeout.ts
498
+ const fetchWithTimeout = async (timeout, resource, options, logger) => {
499
+ const abortController = new AbortController();
500
+ const abortTimeoutId = setTimeout(() => {
501
+ logger.debug("[fetchWithTimeout] Reached timeout, aborting request");
502
+ abortController.abort();
503
+ }, timeout);
504
+ try {
505
+ const response = await fetch(typeof resource === "string" || resource instanceof Request ? resource : resource.toString(), {
506
+ ...options,
507
+ signal: abortController.signal
508
+ });
509
+ clearTimeout(abortTimeoutId);
510
+ return response;
511
+ } catch (error) {
512
+ logger.warn("[fetchWithTimeout] Fetch error: ", error);
513
+ clearTimeout(abortTimeoutId);
514
+ throw error;
515
+ }
516
+ };
517
+ //#endregion
518
+ //#region ../js-server-sdk/src/PollingTransport.ts
519
+ var PollingTransport = class extends AbstractPollingTransport {
520
+ logger;
521
+ eventEmitter = new EventEmitter$1();
522
+ url;
523
+ lastUpdateTimestamp;
524
+ constructor(options) {
525
+ super();
526
+ this.options = options;
527
+ this.options = options;
528
+ this.logger = options.logger;
529
+ this.url = new URL("server/polling/v1", options.baseUrl);
530
+ this.pollingIntervalSeconds = options.pollingInterval ?? 60;
531
+ }
532
+ async connect(timeout) {
533
+ this.clearPollingInterval();
534
+ await this.fetchConfigs(timeout);
535
+ this.schedulePollingInterval(() => {
536
+ this.fetchConfigs(timeout).catch((error) => {
537
+ this.logger.error("[PollingTransport] Error during polling:", error);
538
+ });
539
+ });
540
+ return this;
541
+ }
542
+ async fetchConfigs(timeout) {
543
+ if (this.fatalError) {
544
+ this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
545
+ return;
546
+ }
547
+ try {
548
+ const response = await fetchWithTimeout(timeout, this.url, {
549
+ method: "POST",
550
+ headers: { "Content-Type": "application/json" },
551
+ body: JSON.stringify({
552
+ metaContext: this.options.metaContext,
553
+ serverSdkKey: this.options.serverSdkKey,
554
+ lastUpdateTimestamp: this.lastUpdateTimestamp
555
+ })
556
+ }, this.logger);
557
+ await this.handleNonOkResponse(response);
558
+ if (response.status === 200) {
559
+ const json = JSON.parse(await response.text());
560
+ this.lastUpdateTimestamp = json.timestamp;
561
+ this.eventEmitter.emit("configBundleReceived", json);
562
+ }
563
+ } catch (fetchError) {
564
+ this.handleFetchError(fetchError);
565
+ }
566
+ }
567
+ on(name, handler) {
568
+ this.eventEmitter.on(name, handler);
569
+ }
570
+ off(name, handler) {
571
+ if (handler) this.eventEmitter.off(name, handler);
572
+ else this.eventEmitter.removeAllListeners(name);
573
+ }
574
+ clear() {
575
+ this.eventEmitter.removeAllListeners();
576
+ }
577
+ };
578
+ //#endregion
579
+ //#region ../js-server-sdk/src/OneTimeTransport.ts
580
+ var OneTimeTransport = class extends PollingTransport {
581
+ constructor(options) {
582
+ super(options);
583
+ this.pollingIntervalSeconds = 0;
584
+ }
585
+ };
586
+ //#endregion
443
587
  //#region ../config-evaluator/src/text-comparison.ts
444
588
  const compareText = (value, operator, targetValues) => {
445
589
  switch (operator.toLowerCase()) {
@@ -709,6 +853,7 @@ const parseConfigValue = (configState, defaultValue) => {
709
853
  usedDefault: true,
710
854
  reason: "value-missing"
711
855
  };
856
+ if (configState.type === "json") return parseJson(value, defaultValue, requestedType);
712
857
  if (typeof defaultValue === "string") return {
713
858
  parsedValue: value,
714
859
  requestedType,
@@ -770,6 +915,29 @@ const parseConfigFloat = (value) => {
770
915
  if (isNaN(num) || !isFinite(num)) return;
771
916
  return num;
772
917
  };
918
+ const parseJson = (jsonString, defaultValue, requestedType) => {
919
+ if (typeof defaultValue === "string") return {
920
+ parsedValue: jsonString,
921
+ requestedType,
922
+ usedDefault: false,
923
+ reason: "found-match"
924
+ };
925
+ try {
926
+ return {
927
+ parsedValue: JSON.parse(jsonString),
928
+ requestedType,
929
+ usedDefault: false,
930
+ reason: "found-match"
931
+ };
932
+ } catch {
933
+ return {
934
+ parsedValue: defaultValue,
935
+ requestedType,
936
+ usedDefault: true,
937
+ reason: "invalid-json"
938
+ };
939
+ }
940
+ };
773
941
  //#endregion
774
942
  //#region ../shared/src/logger.ts
775
943
  const LEVELS = {
@@ -840,7 +1008,7 @@ const defaultUrlFactory = (input, base) => new URL(input, base?.toString());
840
1008
  //#region ../shared/src/telemetry/utils.ts
841
1009
  const CONFIG_VALUE_MAX_LENGTH = 500;
842
1010
  const sanitizeValue = (value, type) => {
843
- if (type === "json") try {
1011
+ if (type === "json" || type == null && typeof value === "object") try {
844
1012
  return djb2Hash(JSON.stringify(value));
845
1013
  } catch {
846
1014
  return value.toString().slice(0, CONFIG_VALUE_MAX_LENGTH);
@@ -872,27 +1040,6 @@ const isDroppedEventsEmpty = (droppedEvents) => {
872
1040
  return true;
873
1041
  };
874
1042
  //#endregion
875
- //#region ../shared/src/fetchWithTimeout.ts
876
- const fetchWithTimeout = async (timeout, resource, options, logger) => {
877
- const abortController = new AbortController();
878
- const abortTimeoutId = setTimeout(() => {
879
- logger.debug("[fetchWithTimeout] Reached timeout, aborting request");
880
- abortController.abort();
881
- }, timeout);
882
- try {
883
- const response = await fetch(typeof resource === "string" || resource instanceof Request ? resource : resource.toString(), {
884
- ...options,
885
- signal: abortController.signal
886
- });
887
- clearTimeout(abortTimeoutId);
888
- return response;
889
- } catch (error) {
890
- logger.warn("[fetchWithTimeout] Fetch error: ", error);
891
- clearTimeout(abortTimeoutId);
892
- throw error;
893
- }
894
- };
895
- //#endregion
896
1043
  //#region ../js-server-sdk/src/telemetry/ServerEventReporter.ts
897
1044
  var ServerEventReporter = class {
898
1045
  sdkKey;
@@ -1180,7 +1327,7 @@ var DefaultConfigDirectorClient = class {
1180
1327
  ready = false;
1181
1328
  readyPromise;
1182
1329
  readyResolve;
1183
- streaming;
1330
+ connectionMode;
1184
1331
  configEvaluator;
1185
1332
  metaContext;
1186
1333
  constructor(serverSdkKey, sdkOptions, clientOptions) {
@@ -1189,8 +1336,8 @@ var DefaultConfigDirectorClient = class {
1189
1336
  this.metaContext = clientOptions?.metadata ?? {};
1190
1337
  this.configEvaluator = new ConfigEvaluator(this.logger);
1191
1338
  const baseUrl = this.parseUrl(clientOptions?.connection?.url) ?? defaultBaseUrl;
1192
- this.streaming = clientOptions?.connection?.streaming === false ? false : true;
1193
- const transportConstructor = StreamingTransport;
1339
+ this.connectionMode = clientOptions?.connection?.mode ?? "streaming";
1340
+ const transportConstructor = this.getTransportConstructor(this.connectionMode);
1194
1341
  const queueLimit = clientOptions?.telemetry?.eventQueueLimit ?? DEFAULT_EVENT_QUEUE_LIMIT;
1195
1342
  this.usageEventCollector = new ServerTelemetryEventCollector({
1196
1343
  sdkKey: serverSdkKey,
@@ -1209,7 +1356,8 @@ var DefaultConfigDirectorClient = class {
1209
1356
  sdkName: sdkOptions.sdkName,
1210
1357
  sdkVersion: sdkOptions.sdkVersion
1211
1358
  },
1212
- logger: this.logger
1359
+ logger: this.logger,
1360
+ pollingInterval: clientOptions?.connection?.pollingInterval
1213
1361
  });
1214
1362
  this.transport.on("configBundleReceived", (configBundle) => {
1215
1363
  this.readyResolve?.();
@@ -1247,13 +1395,20 @@ var DefaultConfigDirectorClient = class {
1247
1395
  setTimeout(() => resolve(), remainingTimeout);
1248
1396
  })]);
1249
1397
  if (!this.ready) {
1250
- const warningDetails = this.streaming ? "The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds." : "Since the client was configured without streaming, configs may not update and always return the default value.";
1398
+ const warningDetails = this.connectionMode === "streaming" ? "The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds." : "Since the client was configured without streaming, configs may not update and always return the default value.";
1251
1399
  this.logger.warn(`[ConfigDirectorClient] Timed out waiting for initialization after ${this.timeout}ms. ${warningDetails}`);
1252
1400
  }
1253
1401
  } catch (error) {
1254
1402
  this.logger.error("[ConfigDirectorClient] An error occurred during initialization: ", error);
1255
1403
  }
1256
1404
  }
1405
+ getTransportConstructor(mode) {
1406
+ switch (mode) {
1407
+ case "one-time": return OneTimeTransport;
1408
+ case "polling": return PollingTransport;
1409
+ default: return StreamingTransport;
1410
+ }
1411
+ }
1257
1412
  updateWatchers(configsMap) {
1258
1413
  Object.values(configsMap).forEach((v) => this.updateWatchersForConfig(v));
1259
1414
  }
@@ -1291,14 +1446,16 @@ var DefaultConfigDirectorClient = class {
1291
1446
  }
1292
1447
  getValueFromConfigDefinition(configKey, configDefinition, defaultValue, context) {
1293
1448
  if (configDefinition) {
1294
- const parseResult = parseConfigValue(this.configEvaluator.evaluate(configDefinition, {
1449
+ const configState = this.configEvaluator.evaluate(configDefinition, {
1295
1450
  context,
1296
1451
  metadata: this.metaContext
1297
- }), defaultValue);
1452
+ });
1453
+ const parseResult = parseConfigValue(configState, defaultValue);
1298
1454
  this.usageEventCollector.evaluatedConfig({
1299
1455
  context,
1300
1456
  evaluation: {
1301
1457
  key: configKey,
1458
+ type: configState.type,
1302
1459
  defaultValue,
1303
1460
  requestedType: parseResult.requestedType,
1304
1461
  evaluatedValue: parseResult.parsedValue,
@@ -1354,6 +1511,15 @@ var DefaultConfigDirectorClient = class {
1354
1511
  unwatchAll() {
1355
1512
  this.handlersMap.clear();
1356
1513
  }
1514
+ getAllConfigs(options) {
1515
+ if (!this.configSet) return {};
1516
+ const entries = Object.entries(this.configSet.configs);
1517
+ const filtered = options?.configKeys ? entries.filter(([key]) => options.configKeys?.includes(key)) : entries;
1518
+ return Object.fromEntries(filtered.map(([key, config]) => [key, this.configEvaluator.evaluate(config, {
1519
+ context: options?.context,
1520
+ metadata: this.metaContext
1521
+ })]));
1522
+ }
1357
1523
  closeConnection() {
1358
1524
  this.logger.debug("[ConfigDirectorClient] closeConnection() has been called, closing connection to server");
1359
1525
  this.transport.close();
@@ -1385,7 +1551,7 @@ var plugin_default = defineNitroPlugin((nitroApp) => {
1385
1551
  const { serverSdkKey, baseUrl } = runtimeConfig.configdirector;
1386
1552
  const client = new DefaultConfigDirectorClient(serverSdkKey, {
1387
1553
  sdkName: "nuxt-sdk",
1388
- sdkVersion: "0.1.1"
1554
+ sdkVersion: "0.5.0"
1389
1555
  }, {
1390
1556
  logger,
1391
1557
  ...baseUrl ? { connection: { url: baseUrl } } : {}