@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.
@@ -60,13 +60,6 @@ var ConfigDirectorInitializationError = class ConfigDirectorInitializationError
60
60
  }
61
61
  };
62
62
  //#endregion
63
- //#region ../js-client-core/src/errors.ts
64
- const isFetchErrorFatal = (fetchError) => {
65
- if (fetchError?.name === "NotAllowedError") return true;
66
- else if (fetchError instanceof TypeError) return true;
67
- return false;
68
- };
69
- //#endregion
70
63
  //#region ../eventsource/src/errors.ts
71
64
  var StreamClosedError = class StreamClosedError extends Error {
72
65
  name = "StreamClosedError";
@@ -420,7 +413,7 @@ var StreamingTransport = class {
420
413
  this.dispatchMessage(data);
421
414
  },
422
415
  onConnect: () => {
423
- this.logger.debug("[EventSourceTransport] Connected");
416
+ this.logger.debug("[StreamingTransport] Connected");
424
417
  resolve(this);
425
418
  },
426
419
  shouldReconnect: (state) => {
@@ -430,14 +423,16 @@ var StreamingTransport = class {
430
423
  },
431
424
  calculateReconnectDelay: (state) => {
432
425
  const delay = this.options.connectionRetryDelay(state.attempt);
433
- this.logger.warn(`[EventSourceTransport] Scheduling reconnect in ${delay}ms.`);
426
+ const logMessage = `[StreamingTransport] Scheduling reconnect attempt #${state.attempt} in ${delay}ms.`;
427
+ if (state.attempt <= 5) this.logger.info(logMessage);
428
+ else this.logger.warn(logMessage);
434
429
  return delay;
435
430
  },
436
431
  onError: (error) => {
437
- this.logger.debug("[EventSourceTransport] Error: ", error);
432
+ this.logger.debug("[StreamingTransport] Error: ", error);
438
433
  },
439
434
  onDisconnect: () => {
440
- this.logger.debug("[EventSourceTransport] Disconnected");
435
+ this.logger.debug("[StreamingTransport] Disconnected");
441
436
  }
442
437
  });
443
438
  this.eventSource.connect();
@@ -453,7 +448,7 @@ var StreamingTransport = class {
453
448
  const json = JSON.parse(data);
454
449
  this.eventEmitter.emit("configSetReceived", json);
455
450
  } catch (error) {
456
- this.logger.error("[EventSourceTransport] Error parsing and dispatching config data update: ", error);
451
+ this.logger.error("[StreamingTransport] Error parsing and dispatching config data update: ", error);
457
452
  }
458
453
  }
459
454
  prepareFatalError(responseStatus, error) {
@@ -510,6 +505,7 @@ const parseConfigValue = (configState, defaultValue) => {
510
505
  usedDefault: true,
511
506
  reason: "value-missing"
512
507
  };
508
+ if (configState.type === "json") return parseJson(value, defaultValue, requestedType);
513
509
  if (typeof defaultValue === "string") return {
514
510
  parsedValue: value,
515
511
  requestedType,
@@ -571,6 +567,29 @@ const parseConfigFloat = (value) => {
571
567
  if (isNaN(num) || !isFinite(num)) return;
572
568
  return num;
573
569
  };
570
+ const parseJson = (jsonString, defaultValue, requestedType) => {
571
+ if (typeof defaultValue === "string") return {
572
+ parsedValue: jsonString,
573
+ requestedType,
574
+ usedDefault: false,
575
+ reason: "found-match"
576
+ };
577
+ try {
578
+ return {
579
+ parsedValue: JSON.parse(jsonString),
580
+ requestedType,
581
+ usedDefault: false,
582
+ reason: "found-match"
583
+ };
584
+ } catch {
585
+ return {
586
+ parsedValue: defaultValue,
587
+ requestedType,
588
+ usedDefault: true,
589
+ reason: "invalid-json"
590
+ };
591
+ }
592
+ };
574
593
  //#endregion
575
594
  //#region ../shared/src/logger.ts
576
595
  const LEVELS = {
@@ -635,6 +654,58 @@ const createDefaultLogger$1 = (level, messageDecorator) => {
635
654
  return new DefaultConsoleLogger(level ?? "warn", messageDecorator ?? new LogMessageDecorator());
636
655
  };
637
656
  //#endregion
657
+ //#region ../shared/src/transport/AbstractPollingTransport.ts
658
+ var AbstractPollingTransport = class {
659
+ fatalError = false;
660
+ pollingInterval;
661
+ pollingIntervalSeconds = 60;
662
+ clearPollingInterval() {
663
+ clearInterval(this.pollingInterval);
664
+ }
665
+ schedulePollingInterval(callback) {
666
+ clearInterval(this.pollingInterval);
667
+ if (this.pollingIntervalSeconds <= 0) return;
668
+ this.pollingInterval = setInterval(callback, this.pollingIntervalSeconds * 1e3);
669
+ }
670
+ async handleNonOkResponse(response) {
671
+ if (!response.ok) if (this.isStatusFatal(response.status)) {
672
+ this.fatalError = true;
673
+ throw this.prepareFatalResponseStatusError(response.status, await response.text());
674
+ } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
675
+ }
676
+ handleFetchError(fetchError) {
677
+ if (this.isFetchErrorFatal(fetchError)) {
678
+ this.fatalError = true;
679
+ this.close();
680
+ throw new ConfigDirectorConnectionError(`Connection failed with fatal error: ${fetchError}. This is an unrecoverable error, retry attempts will be ignored.`);
681
+ } else if (fetchError instanceof SyntaxError) throw new ConfigDirectorConnectionError(`Failed to parse the response from the server: ${fetchError}`);
682
+ else throw new ConfigDirectorConnectionError(`Connection failed with error: ${fetchError}.`);
683
+ }
684
+ prepareFatalResponseStatusError(responseStatus, errorBody) {
685
+ const status = responseStatus ?? 0;
686
+ 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);
687
+ }
688
+ isStatusFatal(status) {
689
+ return !!status && status >= 400 && status < 500;
690
+ }
691
+ isFetchErrorFatal(fetchError) {
692
+ if (fetchError?.name === "NotAllowedError") return true;
693
+ else if (fetchError instanceof TypeError) return true;
694
+ return false;
695
+ }
696
+ get isConnected() {
697
+ return this.pollingInterval !== void 0;
698
+ }
699
+ close() {
700
+ clearInterval(this.pollingInterval);
701
+ this.pollingInterval = void 0;
702
+ }
703
+ dispose() {
704
+ this.close();
705
+ this.clear();
706
+ }
707
+ };
708
+ //#endregion
638
709
  //#region ../shared/src/fetchWithTimeout.ts
639
710
  const fetchWithTimeout = async (timeout, resource, options, logger) => {
640
711
  const abortController = new AbortController();
@@ -656,22 +727,34 @@ const fetchWithTimeout = async (timeout, resource, options, logger) => {
656
727
  }
657
728
  };
658
729
  //#endregion
659
- //#region ../js-client-core/src/PullTransport.ts
660
- var PullTransport = class {
730
+ //#region ../js-client-core/src/PollingTransport.ts
731
+ var PollingTransport = class extends AbstractPollingTransport {
661
732
  logger;
662
733
  eventEmitter = new Emitter();
663
734
  url;
664
- fatalError = false;
735
+ lastUpdateTimestamp;
665
736
  constructor(options) {
737
+ super();
666
738
  this.options = options;
667
739
  this.options = options;
668
740
  this.logger = options.logger;
669
- this.url = options.resolveUrl("client/pull/v1", options.baseUrl);
741
+ this.url = options.resolveUrl("client/polling/v1", options.baseUrl);
742
+ this.pollingIntervalSeconds = options.pollingInterval ?? 60;
670
743
  }
671
744
  async connect(context, timeout) {
745
+ this.clearPollingInterval();
746
+ await this.fetchConfigs(context, timeout);
747
+ this.schedulePollingInterval(() => {
748
+ this.fetchConfigs(context, timeout).catch((error) => {
749
+ this.logger.error("[PollingTransport] Error during polling:", error);
750
+ });
751
+ });
752
+ return this;
753
+ }
754
+ async fetchConfigs(context, timeout) {
672
755
  if (this.fatalError) {
673
- this.logger.warn("[PullTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
674
- return this;
756
+ this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
757
+ return;
675
758
  }
676
759
  try {
677
760
  const response = await fetchWithTimeout(timeout, this.url, {
@@ -680,31 +763,20 @@ var PullTransport = class {
680
763
  body: JSON.stringify({
681
764
  givenContext: context,
682
765
  metaContext: this.options.metaContext,
683
- clientSdkKey: this.options.clientSdkKey
766
+ clientSdkKey: this.options.clientSdkKey,
767
+ lastUpdateTimestamp: this.lastUpdateTimestamp
684
768
  })
685
769
  }, this.logger);
686
- if (!response.ok) if (this.isStatusFatal(response.status)) {
687
- this.fatalError = true;
688
- throw this.prepareFatalResponseStatusError(response.status, await response.text());
689
- } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
690
- const json = JSON.parse(await response.text());
691
- this.eventEmitter.emit("configSetReceived", json);
692
- return this;
770
+ await this.handleNonOkResponse(response);
771
+ if (response.status == 200) {
772
+ const json = JSON.parse(await response.text());
773
+ this.lastUpdateTimestamp = json.timestamp;
774
+ this.eventEmitter.emit("configSetReceived", json);
775
+ }
693
776
  } catch (fetchError) {
694
- if (isFetchErrorFatal(fetchError)) {
695
- this.fatalError = true;
696
- throw new ConfigDirectorConnectionError(`Connection failed with fatal error: ${fetchError}. This is an unrecoverable error, retry attempts will be ignored.`);
697
- } else if (fetchError instanceof SyntaxError) throw new ConfigDirectorConnectionError(`Failed to parse the response from the server: ${fetchError}`);
698
- else throw new ConfigDirectorConnectionError(`Connection failed with error: ${fetchError}.`);
777
+ this.handleFetchError(fetchError);
699
778
  }
700
779
  }
701
- prepareFatalResponseStatusError(responseStatus, errorBody) {
702
- const status = responseStatus ?? 0;
703
- 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);
704
- }
705
- isStatusFatal(status) {
706
- return !!status && status >= 400 && status < 500;
707
- }
708
780
  on(name, handler) {
709
781
  this.eventEmitter.on(name, handler);
710
782
  }
@@ -714,10 +786,13 @@ var PullTransport = class {
714
786
  clear() {
715
787
  this.eventEmitter.clear();
716
788
  }
717
- close() {}
718
- dispose() {
719
- this.close();
720
- this.clear();
789
+ };
790
+ //#endregion
791
+ //#region ../js-client-core/src/OneTimeTransport.ts
792
+ var OneTimeTransport = class extends PollingTransport {
793
+ constructor(options) {
794
+ super(options);
795
+ this.pollingIntervalSeconds = 0;
721
796
  }
722
797
  };
723
798
  //#endregion
@@ -748,14 +823,14 @@ var DefaultConfigDirectorClient = class {
748
823
  readyPromise;
749
824
  readyResolve;
750
825
  currentContext;
751
- streaming;
826
+ connectionMode;
752
827
  constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
753
828
  this.logger = clientOptions?.logger ?? createDefaultLogger$1();
754
829
  this.timeout = clientOptions?.connection?.timeout ?? 3e3;
755
830
  const urlFactory = internalClientOptions?.urlFactory ?? defaultUrlFactory;
756
831
  const baseUrl = this.parseUrl(clientOptions?.connection?.url, urlFactory) ?? "https://client-sdk-api.configdirector.com";
757
- this.streaming = clientOptions?.connection?.streaming === false ? false : true;
758
- const transportConstructor = this.streaming ? StreamingTransport : PullTransport;
832
+ this.connectionMode = clientOptions?.connection?.mode ?? "streaming";
833
+ const transportConstructor = this.getTransportConstructor(this.connectionMode);
759
834
  this.telemetryClient = telemetryClient;
760
835
  this.transport = new transportConstructor({
761
836
  clientSdkKey,
@@ -771,7 +846,8 @@ var DefaultConfigDirectorClient = class {
771
846
  fetch: internalClientOptions?.fetch,
772
847
  connectionRetryDelay: internalClientOptions?.connectionRetryDelay ?? ((attempt) => {
773
848
  return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
774
- })
849
+ }),
850
+ pollingInterval: clientOptions?.connection?.pollingInterval
775
851
  });
776
852
  this.transport.on("configSetReceived", (configSet) => {
777
853
  this.readyResolve?.();
@@ -791,6 +867,13 @@ var DefaultConfigDirectorClient = class {
791
867
  this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", { keys: configKeys });
792
868
  });
793
869
  }
870
+ getTransportConstructor(connectionMode) {
871
+ switch (connectionMode) {
872
+ case "one-time": return OneTimeTransport;
873
+ case "polling": return PollingTransport;
874
+ default: return StreamingTransport;
875
+ }
876
+ }
794
877
  async initialize(context) {
795
878
  this.initializing = true;
796
879
  await this.connectToTransport(context, "initialization");
@@ -824,10 +907,7 @@ var DefaultConfigDirectorClient = class {
824
907
  if (remainingTimeout > 0) await Promise.race([this.readyPromise, new Promise((resolve) => {
825
908
  this.timeoutTimer = setTimeout(() => resolve(), remainingTimeout);
826
909
  })]).finally(() => clearTimeout(this.timeoutTimer));
827
- if (!this.ready) {
828
- 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.";
829
- this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. ${warningDetails}`);
830
- }
910
+ if (!this.ready) this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.`);
831
911
  } catch (error) {
832
912
  this.logger.error(`[ConfigDirectorClient] An error occurred during ${caller}: `, error);
833
913
  }
@@ -883,6 +963,7 @@ var DefaultConfigDirectorClient = class {
883
963
  const parseResult = parseConfigValue(configState, defaultValue);
884
964
  this.telemetryClient.evaluatedConfig({
885
965
  contextId: this.currentContext?.id,
966
+ type: configState.type,
886
967
  key: configKey,
887
968
  defaultValue,
888
969
  requestedType: parseResult.requestedType,
@@ -952,7 +1033,7 @@ var DefaultConfigDirectorClient = class {
952
1033
  //#region ../shared/src/telemetry/utils.ts
953
1034
  const CONFIG_VALUE_MAX_LENGTH = 500;
954
1035
  const sanitizeValue = (value, type) => {
955
- if (type === "json") try {
1036
+ if (type === "json" || type == null && typeof value === "object") try {
956
1037
  return djb2Hash(JSON.stringify(value));
957
1038
  } catch {
958
1039
  return value.toString().slice(0, CONFIG_VALUE_MAX_LENGTH);
@@ -990,7 +1071,7 @@ var WebWorkerTelemetryClient = class {
990
1071
  flushIntervalDelay: options.flushIntervalDelay ?? 3e4
991
1072
  }
992
1073
  };
993
- this.worker = new Worker(URL.createObjectURL(new Blob(["(function(){let e=(e,n)=>{if(n===`json`)try{return t(JSON.stringify(e))}catch{return e.toString().slice(0,500)}return e.toString().slice(0,500)},t=e=>n(new TextEncoder().encode(e)).toString(16).padStart(8,`0`),n=e=>{let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+(e[n]??0),t>>>=0;return t},r=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},i=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},a=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(typeof t==`string`||t instanceof Request?t:t.toString(),{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}},o=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var s=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=e.urlFactory(`client/telemetry/v1`,e.baseUrl)}async report({context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r}){if(!this.executeRequests)return{success:!1,fatalError:!0};let i={clientSdkKey:this.sdkKey,context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r};if(this.isReportEmpty(i))return{success:!0,fatalError:!1};let a=await this.sendReport(i);return a.fatalError&&(this.executeRequests=!1),a}isReportEmpty(e){return r(e.discreteEvents)&&r(e.aggregatedEvents)&&i(e.droppedEvents)}async sendReport(e){try{let t=await a(5e3,this.url,{method:`POST`,body:JSON.stringify(e),keepalive:!0},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){let t=o(e);return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:t}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},c=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},l=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},u=class{constructor(e){this.aggregator=new c,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new l(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}sanitizeValue(t,n){return e(t,n)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot();return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),await this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},d=class extends u{constructor(e){super(e),this.reporter=new s(e)}async updateContext(e){this.cancelScheduledFlush(),await this.flush(),this._context=e,await this.flushAndScheduleNext()}evaluatedConfig(e){this.collectEvents&&this.evaluationEventQueue.push(e)}async forceFlush(){this.cancelScheduledFlush(),await this.flushAndScheduleNext()}};let f=(e,t)=>new URL(e,t?.toString()),p,m=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),h=e=>{try{self.postMessage(e)}catch{self.postMessage(m(e.payload.level,e.payload.message))}},g={debug:function(e,...t){h(m(`debug`,e,t))},info:function(e,...t){h(m(`info`,e,t))},warn:function(e,...t){h(m(`warn`,e,t))},error:function(e,...t){h(m(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!p){g.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(g.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:p=new d({...t.payload,baseUrl:f(t.payload.baseUrl),urlFactory:f,logger:g});break;case`EvaluatedConfigEvent`:p?.evaluatedConfig(t.payload);break;case`UpdateContext`:p?.updateContext(t.payload.context);break;case`Flush`:p?.forceFlush();break;case`Close`:(p?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
1074
+ this.worker = new Worker(URL.createObjectURL(new Blob(["(function(){let e=(e,n)=>{if(n===`json`||n==null&&typeof e==`object`)try{return t(JSON.stringify(e))}catch{return e.toString().slice(0,500)}return e.toString().slice(0,500)},t=e=>n(new TextEncoder().encode(e)).toString(16).padStart(8,`0`),n=e=>{let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t+(e[n]??0),t>>>=0;return t},r=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},i=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},a=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(typeof t==`string`||t instanceof Request?t:t.toString(),{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}},o=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var s=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.url=e.urlFactory(`client/telemetry/v1`,e.baseUrl)}async report({context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r}){if(!this.executeRequests)return{success:!1,fatalError:!0};let i={clientSdkKey:this.sdkKey,context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r};if(this.isReportEmpty(i))return{success:!0,fatalError:!1};let a=await this.sendReport(i);return a.fatalError&&(this.executeRequests=!1),a}isReportEmpty(e){return r(e.discreteEvents)&&r(e.aggregatedEvents)&&i(e.droppedEvents)}async sendReport(e){try{let t=await a(5e3,this.url,{method:`POST`,body:JSON.stringify(e),keepalive:!0},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){let t=o(e);return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:t}}}isStatusFatal(e){return!!e&&e>=400&&e<500}},c=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},l=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},u=class{constructor(e){this.aggregator=new c,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new l(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}sanitizeValue(t,n){return e(t,n)}async flushAndScheduleNext(){(await this.flush()).fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay)}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=this.evaluationEventQueue.takeSnapshot();return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),await this.flush(),this.evaluationEventQueue.clear()}dispose(){this.close()}},d=class extends u{constructor(e){super(e),this.reporter=new s(e)}async updateContext(e){this.cancelScheduledFlush(),await this.flush(),this._context=e,await this.flushAndScheduleNext()}evaluatedConfig(e){this.collectEvents&&this.evaluationEventQueue.push(e)}async forceFlush(){this.cancelScheduledFlush(),await this.flushAndScheduleNext()}};let f=(e,t)=>new URL(e,t?.toString()),p,m=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),h=e=>{try{self.postMessage(e)}catch{self.postMessage(m(e.payload.level,e.payload.message))}},g={debug:function(e,...t){h(m(`debug`,e,t))},info:function(e,...t){h(m(`info`,e,t))},warn:function(e,...t){h(m(`warn`,e,t))},error:function(e,...t){h(m(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!p){g.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(g.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:p=new d({...t.payload,baseUrl:f(t.payload.baseUrl),urlFactory:f,logger:g});break;case`EvaluatedConfigEvent`:p?.evaluatedConfig(t.payload);break;case`UpdateContext`:p?.updateContext(t.payload.context);break;case`Flush`:p?.forceFlush();break;case`Close`:(p?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
994
1075
  this.worker.onmessage = (message) => {
995
1076
  this.handleWorkerEvent(message.data);
996
1077
  };
@@ -1130,7 +1211,7 @@ var plugin_client_default = defineNuxtPlugin((nuxtApp) => {
1130
1211
  const { clientSdkKey, appName, appVersion, baseUrl } = runtimeConfig.public.configdirector;
1131
1212
  const client = createBrowserClient(clientSdkKey, {
1132
1213
  sdkName: "nuxt-sdk",
1133
- sdkVersion: "0.1.1"
1214
+ sdkVersion: "0.5.0"
1134
1215
  }, {
1135
1216
  metadata: {
1136
1217
  appName,
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../../types-D9MFb76Q.mjs";
1
+ import { t as ConfigDirectorClient } from "../../types-DGQ1B6mZ.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => ConfigDirectorClient;
@@ -1,4 +1,4 @@
1
- import { t as ConfigDirectorClient } from "../../types-D9MFb76Q.mjs";
1
+ import { t as ConfigDirectorClient } from "../../types-DGQ1B6mZ.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => ConfigDirectorClient;
@@ -1,8 +1,16 @@
1
1
  //#region ../shared/src/types.d.ts
2
+ declare const ConfigTypeList: readonly ["custom", "boolean", "string", "integer", "float", "enum", "url", "json"];
3
+ type ConfigType = (typeof ConfigTypeList)[number];
2
4
  type ConfigEnumLikeType = {
3
5
  [key: string]: string | number;
4
6
  };
5
7
  type ConfigValueType = string | number | boolean | object | ConfigEnumLikeType;
8
+ type ConfigState = {
9
+ id: string;
10
+ key: string;
11
+ type: ConfigType;
12
+ value: string | undefined | null;
13
+ };
6
14
  /**
7
15
  * The user's context to be sent to ConfigDirector. This context will be used for targeting
8
16
  * rules evaluation.
@@ -31,4 +39,4 @@ type ConfigDirectorContext = {
31
39
  };
32
40
  };
33
41
  //#endregion
34
- export { ConfigValueType as n, ConfigDirectorContext as t };
42
+ export { ConfigState as n, ConfigValueType as r, ConfigDirectorContext as t };
@@ -1,4 +1,4 @@
1
- import { n as ConfigValueType, t as ConfigDirectorContext } from "./types-CQWNx-gI.mjs";
1
+ import { r as ConfigValueType, t as ConfigDirectorContext } from "./types-BWxsGnk2.mjs";
2
2
 
3
3
  //#region ../js-client-core/src/Emitter.d.ts
4
4
  type EventType = string | symbol;
@@ -1,4 +1,4 @@
1
- import { n as ConfigValueType, t as ConfigDirectorContext } from "./types-CQWNx-gI.mjs";
1
+ import { n as ConfigState, r as ConfigValueType, t as ConfigDirectorContext } from "./types-BWxsGnk2.mjs";
2
2
 
3
3
  //#region ../js-server-sdk/src/types.d.ts
4
4
  type ClientEvents = {
@@ -71,6 +71,17 @@ interface ConfigDirectorClient {
71
71
  * Removes all subscribers from all config keys
72
72
  */
73
73
  unwatchAll(): void;
74
+ /**
75
+ * Returns the evaluated {@link ConfigState} for every known config key, or only the keys
76
+ * listed in `configKeys` when provided.
77
+ *
78
+ * Intended for SSR hydration — does NOT record telemetry events. Returns an empty object
79
+ * when the client is not yet ready.
80
+ */
81
+ getAllConfigs(options?: {
82
+ context?: ConfigDirectorContext;
83
+ configKeys?: string[];
84
+ }): Record<string, ConfigState>;
74
85
  on<T extends keyof ClientEvents>(eventName: T, handler: (args: ClientEvents[T]) => void): void;
75
86
  off<T extends keyof ClientEvents>(eventName: T, handler?: (args: ClientEvents[T]) => void): void;
76
87
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/nuxt-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.5.0",
4
4
  "description": "Nuxt SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",
@@ -18,7 +18,7 @@
18
18
  "homepage": "https://www.configdirector.com",
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/ConfigDirector/js-sdks"
21
+ "url": "git+https://github.com/ConfigDirector/js-sdks.git"
22
22
  },
23
23
  "type": "module",
24
24
  "exports": {
@@ -41,7 +41,7 @@
41
41
  "scripts": {
42
42
  "build": "nuxt prepare && tsdown",
43
43
  "prepack": "nuxt prepare && tsdown",
44
- "release": "yarn lint && yarn ci:test && yarn build && npm publish --access public",
44
+ "release": "nuxt prepare && yarn lint && yarn ci:test && yarn build && npm publish --access public",
45
45
  "lint": "eslint .",
46
46
  "ci:test": "vitest run",
47
47
  "test": "vitest watch",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@jsonjoy.com/json-pointer": "^18.1.0",
52
- "@nuxt/kit": "^4.4.2",
52
+ "@nuxt/kit": "^3.7.0 || ^4.0.0",
53
53
  "consola": "^3.4.2",
54
54
  "defu": "^6.1.7",
55
55
  "rapidhash-js": "^3.0.0",
@@ -63,14 +63,14 @@
63
63
  "@types/node": "latest",
64
64
  "changelogen": "^0.6.2",
65
65
  "eslint": "^10.2.1",
66
- "nuxt": "^4.4.2",
67
- "playwright": "^1.52.0",
66
+ "nuxt": "^4.4.6",
67
+ "playwright": "^1.60.0",
68
68
  "rimraf": "^6.1.3",
69
69
  "tsdown": "^0.21.7",
70
70
  "tslib": "^2.8.1",
71
71
  "typescript": "~6.0.3",
72
72
  "vite": "^8.0.10",
73
- "vitest": "^4.1.4",
73
+ "vitest": "^4.1.8",
74
74
  "vue-tsc": "^3.2.7"
75
75
  }
76
76
  }