@configdirector/nuxt-sdk 1.7.0 → 1.8.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.
@@ -512,7 +512,7 @@ const configTypesNotReadableAsString = new Set([
512
512
  const parseConfigValue = (configState, defaultValue) => {
513
513
  const value = configState.value;
514
514
  const requestedType = getRequestedType(defaultValue);
515
- if (value == null) return {
515
+ if (value == null || value === "") return {
516
516
  parsedValue: defaultValue,
517
517
  parsedValueId: void 0,
518
518
  requestedType,
@@ -715,6 +715,22 @@ const parseUrl = (url) => {
715
715
  }
716
716
  };
717
717
  //#endregion
718
+ //#region ../shared/src/transport/pollingInterval.ts
719
+ const resolvePollingInterval = (configuredSeconds, defaults) => {
720
+ if (configuredSeconds === void 0) return {
721
+ seconds: defaults.defaultSeconds,
722
+ warning: void 0
723
+ };
724
+ if (configuredSeconds >= defaults.minimumSeconds) return {
725
+ seconds: configuredSeconds,
726
+ warning: void 0
727
+ };
728
+ return {
729
+ seconds: defaults.minimumSeconds,
730
+ warning: `pollingInterval of ${configuredSeconds} seconds is below the minimum of ${defaults.minimumSeconds} seconds. Using ${defaults.minimumSeconds} seconds.`
731
+ };
732
+ };
733
+ //#endregion
718
734
  //#region ../shared/src/instance-id.ts
719
735
  const generateInstanceId = () => {
720
736
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
@@ -813,7 +829,6 @@ const resolveInstanceId = (logger) => {
813
829
  var AbstractPollingTransport = class {
814
830
  fatalError = false;
815
831
  pollingInterval;
816
- pollingIntervalSeconds = 60;
817
832
  clearPollingInterval() {
818
833
  clearInterval(this.pollingInterval);
819
834
  }
@@ -883,6 +898,7 @@ const fetchWithTimeout = async (timeout, resource, options, logger) => {
883
898
  //#endregion
884
899
  //#region ../js-client-core/src/PollingTransport.ts
885
900
  var PollingTransport = class extends AbstractPollingTransport {
901
+ pollingIntervalSeconds;
886
902
  logger;
887
903
  eventEmitter = new Emitter();
888
904
  url;
@@ -893,9 +909,7 @@ var PollingTransport = class extends AbstractPollingTransport {
893
909
  this.options = options;
894
910
  this.logger = options.logger;
895
911
  this.url = options.resolveUrl("client/polling/v1", options.baseUrl);
896
- const requestedInterval = options.pollingInterval ?? 300;
897
- if (requestedInterval < 60) this.logger.warn(`[PollingTransport] pollingInterval of ${requestedInterval} seconds is below the minimum of 60 seconds. Using 60 seconds.`);
898
- this.pollingIntervalSeconds = Math.max(requestedInterval, 60);
912
+ this.pollingIntervalSeconds = options.pollingInterval ?? 60;
899
913
  }
900
914
  async connect(context, timeout) {
901
915
  this.clearPollingInterval();
@@ -977,6 +991,7 @@ var DefaultConfigDirectorClient = class {
977
991
  timeoutTimer;
978
992
  ready = false;
979
993
  initializing = false;
994
+ hasReceivedConfigSet = false;
980
995
  readyPromise;
981
996
  readyResolve;
982
997
  currentContext;
@@ -990,45 +1005,30 @@ var DefaultConfigDirectorClient = class {
990
1005
  const urlFactory = internalClientOptions?.urlFactory ?? defaultUrlFactory;
991
1006
  const baseUrl = this.parseUrl(clientOptions?.connection?.url, urlFactory) ?? "https://client-sdk-api.configdirector.com";
992
1007
  this.connectionMode = clientOptions?.connection?.mode ?? "streaming";
993
- const transportConstructor = this.getTransportConstructor(this.connectionMode);
994
1008
  this.telemetryClient = telemetryClient;
995
- this.transport = new transportConstructor({
996
- instanceId: this.instanceId,
997
- clientSdkKey,
998
- baseUrl,
999
- resolveUrl: urlFactory,
1000
- metaContext: {
1001
- ...clientOptions?.metadata,
1002
- sdkName: sdkOptions.sdkName,
1003
- sdkVersion: sdkOptions.sdkVersion,
1004
- userAgent: readUserAgent(),
1005
- host: readHost()
1006
- },
1007
- logger: this.logger,
1008
- fetch: internalClientOptions?.fetch,
1009
- connectionRetryDelay: internalClientOptions?.connectionRetryDelay ?? ((attempt) => {
1010
- return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1011
- }),
1012
- pollingInterval: clientOptions?.connection?.pollingInterval
1013
- });
1009
+ this.transport = internalClientOptions?.transport ?? this.buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions);
1014
1010
  this.transport.on("configSetReceived", (configSet) => {
1011
+ this.hasReceivedConfigSet = true;
1015
1012
  this.readyResolve?.();
1016
- const configKeys = Object.keys(configSet.configs);
1017
- if (!this.configSet || configSet.kind == "full") {
1018
- this.configSet = configSet;
1019
- this.emit("configsUpdated", { keys: configKeys });
1020
- this.updateWatchers(configSet.configs);
1021
- } else {
1022
- this.configSet.configs = {
1023
- ...this.configSet.configs,
1024
- ...configSet.configs
1025
- };
1026
- this.emit("configsUpdated", { keys: configKeys });
1027
- this.updateWatchers(configSet.configs);
1028
- }
1029
- this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", { keys: configKeys });
1013
+ const removedKeys = this.findKeysRemovedBy(configSet);
1014
+ if (!this.configSet || configSet.kind == "full") this.configSet = configSet;
1015
+ else this.configSet.configs = {
1016
+ ...this.configSet.configs,
1017
+ ...configSet.configs
1018
+ };
1019
+ const keys = Object.keys(configSet.configs);
1020
+ this.emit("configsUpdated", {
1021
+ keys,
1022
+ removedKeys
1023
+ });
1024
+ this.updateWatchers([...keys, ...removedKeys]);
1025
+ this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", {
1026
+ keys,
1027
+ removedKeys
1028
+ });
1030
1029
  });
1031
1030
  this.transport.on("connectionError", (error) => {
1031
+ this.initializing = false;
1032
1032
  this.logger.error("[ConfigDirectorClient] The connection encountered an unrecoverable error and will not be retried. Configs will no longer receive updates.", error);
1033
1033
  this.emit("connectionError", { error });
1034
1034
  });
@@ -1043,6 +1043,36 @@ var DefaultConfigDirectorClient = class {
1043
1043
  const handlers = Array.isArray(handler) ? handler : [handler];
1044
1044
  for (const h of handlers) if (typeof h === "function") this.on(event, h);
1045
1045
  }
1046
+ resolvePollingIntervalForMode(configuredSeconds) {
1047
+ if (this.connectionMode !== "polling") return;
1048
+ const resolved = resolvePollingInterval(configuredSeconds, {
1049
+ defaultSeconds: 60,
1050
+ minimumSeconds: 30
1051
+ });
1052
+ if (resolved.warning) this.logger.warn(`[ConfigDirectorClient] ${resolved.warning}`);
1053
+ return resolved.seconds;
1054
+ }
1055
+ buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions) {
1056
+ return new (this.getTransportConstructor(this.connectionMode))({
1057
+ instanceId: this.instanceId,
1058
+ clientSdkKey,
1059
+ baseUrl,
1060
+ resolveUrl: urlFactory,
1061
+ metaContext: {
1062
+ ...clientOptions?.metadata,
1063
+ sdkName: sdkOptions.sdkName,
1064
+ sdkVersion: sdkOptions.sdkVersion,
1065
+ userAgent: readUserAgent(),
1066
+ host: readHost()
1067
+ },
1068
+ logger: this.logger,
1069
+ fetch: internalClientOptions?.fetch,
1070
+ connectionRetryDelay: internalClientOptions?.connectionRetryDelay ?? ((attempt) => {
1071
+ return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1072
+ }),
1073
+ pollingInterval: this.resolvePollingIntervalForMode(clientOptions?.connection?.pollingInterval)
1074
+ });
1075
+ }
1046
1076
  getTransportConstructor(connectionMode) {
1047
1077
  switch (connectionMode) {
1048
1078
  case "polling": return PollingTransport;
@@ -1050,7 +1080,7 @@ var DefaultConfigDirectorClient = class {
1050
1080
  }
1051
1081
  }
1052
1082
  async initialize(context) {
1053
- this.initializing = true;
1083
+ if (!this.hasReceivedConfigSet) this.initializing = true;
1054
1084
  await this.connectToTransport(context, "initialization");
1055
1085
  }
1056
1086
  async updateContext(context) {
@@ -1072,7 +1102,7 @@ var DefaultConfigDirectorClient = class {
1072
1102
  this.logger.debug("[ConfigDirectorClient] Received initial payload from the server, client is ready");
1073
1103
  });
1074
1104
  const startTime = (/* @__PURE__ */ new Date()).getTime();
1075
- await this.transport.connect(context ?? {}, this.timeout);
1105
+ await this.transport.connect(context ?? {}, this.timeout, caller);
1076
1106
  this.currentContext = context;
1077
1107
  this.telemetryClient.updateContext(context);
1078
1108
  this.emit("contextUpdated", { context });
@@ -1088,12 +1118,17 @@ var DefaultConfigDirectorClient = class {
1088
1118
  this.logger.error(`[ConfigDirectorClient] An error occurred during ${caller}: `, error);
1089
1119
  }
1090
1120
  }
1091
- updateWatchers(configsMap) {
1092
- Object.values(configsMap).forEach((v) => this.updateWatchersForConfig(v));
1121
+ findKeysRemovedBy(configSet) {
1122
+ if (!this.configSet || configSet.kind != "full") return [];
1123
+ return Object.keys(this.configSet.configs).filter((key) => !Object.prototype.hasOwnProperty.call(configSet.configs, key));
1124
+ }
1125
+ updateWatchers(configKeys) {
1126
+ configKeys.forEach((configKey) => this.updateWatchersForConfig(configKey));
1093
1127
  }
1094
- updateWatchersForConfig(configState) {
1095
- this.handlersMap.get(configState.key)?.forEach((h) => {
1096
- const value = this.getValueFromConfigState(configState.key, configState, h.defaultValue);
1128
+ updateWatchersForConfig(configKey) {
1129
+ const configState = this.configSet?.configs[configKey];
1130
+ this.handlersMap.get(configKey)?.forEach((h) => {
1131
+ const value = this.getValueFromConfigState(configKey, configState, h.defaultValue);
1097
1132
  h.handler(value);
1098
1133
  });
1099
1134
  }
@@ -1266,7 +1301,10 @@ const mapToTelemetryValue = (ev) => {
1266
1301
  const DEFAULT_WORKER_CLOSE_TIMEOUT = 6e3;
1267
1302
  var WebWorkerTelemetryClient = class {
1268
1303
  logger;
1304
+ initializeMessage;
1269
1305
  worker;
1306
+ pendingEvaluations = [];
1307
+ pendingEvaluationLimit;
1270
1308
  closePromise;
1271
1309
  closeResolve;
1272
1310
  closeTimer;
@@ -1275,45 +1313,59 @@ var WebWorkerTelemetryClient = class {
1275
1313
  constructor(options) {
1276
1314
  this.logger = options.logger;
1277
1315
  this.workerCloseTimeout = options.workerCloseTimeout ?? DEFAULT_WORKER_CLOSE_TIMEOUT;
1278
- const initializeMessage = {
1316
+ this.pendingEvaluationLimit = options.evaluationQueueLimit ?? 1e3;
1317
+ this.initializeMessage = {
1279
1318
  type: "Initialize",
1280
1319
  payload: {
1281
1320
  sdkKey: options.sdkKey,
1282
1321
  sdkIdentity: options.sdkIdentity,
1283
1322
  metaContext: options.metaContext,
1284
1323
  baseUrl: options.baseUrl.toString(),
1285
- evaluationQueueLimit: options.evaluationQueueLimit ?? 1e3,
1324
+ evaluationQueueLimit: this.pendingEvaluationLimit,
1286
1325
  initialFlushIntervalDelay: options.initialFlushIntervalDelay ?? 5e3,
1287
1326
  flushIntervalDelay: options.flushIntervalDelay ?? 3e4
1288
1327
  }
1289
1328
  };
1290
- this.worker = new Worker(URL.createObjectURL(new Blob(["(function(){let e=async(e,t)=>e.valueId?{valueId:e.valueId}:e.value?e.value.length<=500&&e.type!=`json`?{value:e.value}:{valueId:await t(e.value)}:e,t=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},n=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},r=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}},i=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var a=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.sdkIdentity=e.sdkIdentity,this.metaContext=e.metaContext,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,metaContext:{sdkName:this.sdkIdentity.sdkName,sdkVersion:this.sdkIdentity.sdkVersion,appName:this.metaContext.appName,appVersion:this.metaContext.appVersion},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 t(e.discreteEvents)&&t(e.aggregatedEvents)&&n(e.droppedEvents)}async sendReport(e){try{let t=await r(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=i(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&&e!=429}},o=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}))}},s=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}},c=class{constructor(e){this.aggregator=new o,this.collectEvents=!0,this.logger=e.logger,this.evaluationEventQueue=new s(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4;let t=e.initialFlushIntervalDelay??5e3;this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),t)}async flushAndScheduleNext(){if(!this.collectEvents)return;let e;try{e=await this.flush()}catch(t){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events: `,t),e={success:!1,fatalError:!1}}e.fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.collectEvents&&(this.cancelScheduledFlush(),this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay))}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout);try{await this.flush()}catch(e){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events during close: `,e)}this.evaluationEventQueue.clear()}dispose(){this.close()}},l=class{constructor(e){this.valueIdGenerator=e,this.valueIdGenerator=e}async process(t){let n=await Promise.all(t.events.map(async t=>({...t,defaultValue:await e(t.defaultValue,this.valueIdGenerator),evaluatedValue:await e(t.evaluatedValue,this.valueIdGenerator)})));return{...t,events:n}}},u=class extends c{constructor(e){super(e),this.reporter=new a(e),this.evaluationEventSnapshotPreprocessor=new l(e.valueIdGenerator)}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()}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=await this.evaluationEventSnapshotPreprocessor.process(this.evaluationEventQueue.takeSnapshot());return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}};let d=(e,t)=>new URL(e,t?.toString()),f=(e,t)=>{let n=BigInt(0),r=BigInt(62),i=Array.from(e).map(e=>e.toString(16).padStart(2,`0`)).join(``),a=BigInt(`0x`+i),o=``;for(;a>n;)o=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`[Number(a%r)]+o,a/=r;return o.padStart(t,`0`)},p=Math.ceil(128/Math.log2(62)),m=new TextEncoder,h=async e=>{if(typeof crypto>`u`||crypto.subtle==null)return;let t=await crypto.subtle.digest(`SHA-256`,m.encode(e?.toString()??``));return f(new Uint8Array(t,0,16),p)},g,_=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),v=e=>{try{self.postMessage(e)}catch{self.postMessage(_(e.payload.level,e.payload.message))}},y={debug:function(e,...t){v(_(`debug`,e,t))},info:function(e,...t){v(_(`info`,e,t))},warn:function(e,...t){v(_(`warn`,e,t))},error:function(e,...t){v(_(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!g){y.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(y.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:if(g){y.warn(`[TelemetryWorker] Received a duplicate Initialize event, ignoring it`);break}g=new u({...t.payload,baseUrl:d(t.payload.baseUrl),urlFactory:d,valueIdGenerator:h,logger:y});break;case`EvaluatedConfigEvent`:g?.evaluatedConfig(t.payload);break;case`UpdateContext`:g?.updateContext(t.payload.context);break;case`Flush`:g?.forceFlush();break;case`Close`:(g?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
1291
- this.worker.onmessage = (message) => {
1292
- this.handleWorkerEvent(message.data);
1293
- };
1294
- this.worker.postMessage(initializeMessage);
1295
1329
  this.visibilityHandler = () => {
1296
1330
  if (document.visibilityState === "hidden") this.flush();
1297
1331
  };
1332
+ }
1333
+ startWorker() {
1334
+ if (this.worker) return this.worker;
1335
+ const worker = new Worker(URL.createObjectURL(new Blob(["(function(){let e=async(e,t)=>e.valueId?{valueId:e.valueId}:e.value?e.value.length<=500&&e.type!=`json`?{value:e.value}:{valueId:await t(e.value)}:e,t=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},n=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},r=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}},i=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var a=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.sdkIdentity=e.sdkIdentity,this.metaContext=e.metaContext,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,metaContext:{sdkName:this.sdkIdentity.sdkName,sdkVersion:this.sdkIdentity.sdkVersion,appName:this.metaContext.appName,appVersion:this.metaContext.appVersion},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 t(e.discreteEvents)&&t(e.aggregatedEvents)&&n(e.droppedEvents)}async sendReport(e){try{let t=await r(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=i(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&&e!=429}},o=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}))}},s=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}},c=class{constructor(e){this.aggregator=new o,this.collectEvents=!0,this.started=!1,this.logger=e.logger,this.evaluationEventQueue=new s(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4,this.initialFlushIntervalDelay=e.initialFlushIntervalDelay??5e3}start(){this.started||!this.collectEvents||(this.started=!0,this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.initialFlushIntervalDelay))}async flushAndScheduleNext(){if(!this.collectEvents)return;let e;try{e=await this.flush()}catch(t){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events: `,t),e={success:!1,fatalError:!1}}e.fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.collectEvents&&(this.cancelScheduledFlush(),this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay))}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout);try{await this.flush()}catch(e){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events during close: `,e)}this.evaluationEventQueue.clear()}dispose(){this.close()}},l=class{constructor(e){this.valueIdGenerator=e,this.valueIdGenerator=e}async process(t){let n=await Promise.all(t.events.map(async t=>({...t,defaultValue:await e(t.defaultValue,this.valueIdGenerator),evaluatedValue:await e(t.evaluatedValue,this.valueIdGenerator)})));return{...t,events:n}}},u=class extends c{constructor(e){super(e),this.reporter=new a(e),this.evaluationEventSnapshotPreprocessor=new l(e.valueIdGenerator)}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()}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=await this.evaluationEventSnapshotPreprocessor.process(this.evaluationEventQueue.takeSnapshot());return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}};let d=(e,t)=>new URL(e,t?.toString()),f=(e,t)=>{let n=BigInt(0),r=BigInt(62),i=Array.from(e).map(e=>e.toString(16).padStart(2,`0`)).join(``),a=BigInt(`0x`+i),o=``;for(;a>n;)o=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`[Number(a%r)]+o,a/=r;return o.padStart(t,`0`)},p=Math.ceil(128/Math.log2(62)),m=new TextEncoder,h=async e=>{if(typeof crypto>`u`||crypto.subtle==null)return;let t=await crypto.subtle.digest(`SHA-256`,m.encode(e?.toString()??``));return f(new Uint8Array(t,0,16),p)},g,_=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),v=e=>{try{self.postMessage(e)}catch{self.postMessage(_(e.payload.level,e.payload.message))}},y={debug:function(e,...t){v(_(`debug`,e,t))},info:function(e,...t){v(_(`info`,e,t))},warn:function(e,...t){v(_(`warn`,e,t))},error:function(e,...t){v(_(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!g){y.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(y.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:if(g){y.warn(`[TelemetryWorker] Received a duplicate Initialize event, ignoring it`);break}g=new u({...t.payload,baseUrl:d(t.payload.baseUrl),urlFactory:d,valueIdGenerator:h,logger:y}),g.start();break;case`EvaluatedConfigEvent`:g?.evaluatedConfig(t.payload);break;case`UpdateContext`:g?.updateContext(t.payload.context);break;case`Flush`:g?.forceFlush();break;case`Close`:(g?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
1336
+ worker.onmessage = (message) => {
1337
+ this.handleWorkerEvent(message.data);
1338
+ };
1339
+ worker.postMessage(this.initializeMessage);
1340
+ this.worker = worker;
1298
1341
  try {
1299
1342
  document.addEventListener("visibilitychange", this.visibilityHandler);
1300
1343
  } catch (error) {
1301
1344
  this.logger.warn("[TelemetryEventCollector] Could not configure 'visibilitychange' listener: ", error);
1302
1345
  }
1346
+ for (const evaluation of this.pendingEvaluations) worker.postMessage(evaluation);
1347
+ this.pendingEvaluations.length = 0;
1348
+ return worker;
1303
1349
  }
1304
1350
  async updateContext(value) {
1351
+ if (this.closePromise) return;
1305
1352
  const updateContextMessage = {
1306
1353
  type: "UpdateContext",
1307
1354
  payload: { context: value }
1308
1355
  };
1309
- this.worker.postMessage(updateContextMessage);
1356
+ this.startWorker().postMessage(updateContextMessage);
1310
1357
  }
1311
1358
  evaluatedConfig(event) {
1312
1359
  const evaluatedConfigMessage = {
1313
1360
  type: "EvaluatedConfigEvent",
1314
1361
  payload: this.sanitizeEvaluatedConfigEvent(event)
1315
1362
  };
1316
- this.worker.postMessage(evaluatedConfigMessage);
1363
+ if (this.worker) {
1364
+ this.worker.postMessage(evaluatedConfigMessage);
1365
+ return;
1366
+ }
1367
+ if (this.pendingEvaluations.length >= this.pendingEvaluationLimit) this.pendingEvaluations.shift();
1368
+ this.pendingEvaluations.push(evaluatedConfigMessage);
1317
1369
  }
1318
1370
  sanitizeEvaluatedConfigEvent(event) {
1319
1371
  return {
@@ -1352,13 +1404,19 @@ var WebWorkerTelemetryClient = class {
1352
1404
  }
1353
1405
  }
1354
1406
  flush() {
1355
- this.worker.postMessage({ type: "Flush" });
1407
+ this.worker?.postMessage({ type: "Flush" });
1356
1408
  }
1357
1409
  close() {
1358
1410
  if (this.closePromise) return this.closePromise;
1359
1411
  return this.performClose();
1360
1412
  }
1361
1413
  performClose() {
1414
+ const worker = this.worker;
1415
+ if (!worker) {
1416
+ this.pendingEvaluations.length = 0;
1417
+ this.closePromise = Promise.resolve();
1418
+ return this.closePromise;
1419
+ }
1362
1420
  try {
1363
1421
  document.removeEventListener("visibilitychange", this.visibilityHandler);
1364
1422
  } catch (error) {
@@ -1370,14 +1428,14 @@ var WebWorkerTelemetryClient = class {
1370
1428
  this.logger.warn("[WebWorkerTelemetryClient] Timed out waiting for the telemetry worker to close. Terminating the worker.");
1371
1429
  this.finishClose();
1372
1430
  }, this.workerCloseTimeout);
1373
- this.worker.postMessage({ type: "Close" });
1431
+ worker.postMessage({ type: "Close" });
1374
1432
  });
1375
1433
  return this.closePromise;
1376
1434
  }
1377
1435
  finishClose() {
1378
1436
  clearTimeout(this.closeTimer);
1379
1437
  this.closeTimer = void 0;
1380
- this.worker.terminate();
1438
+ this.worker?.terminate();
1381
1439
  this.closeResolve?.();
1382
1440
  }
1383
1441
  };
@@ -1430,6 +1488,35 @@ const createBrowserClient = (clientSdkKey, sdkOptions, clientOptions) => {
1430
1488
  });
1431
1489
  };
1432
1490
  //#endregion
1491
+ //#region ../js-client-core/src/installedTestClient.ts
1492
+ const INSTALLED_TEST_CLIENT_SLOT = Symbol.for("@configdirector/installed-test-client");
1493
+ const readInstalledTestClient = () => globalThis[INSTALLED_TEST_CLIENT_SLOT];
1494
+ //#endregion
1495
+ //#region ../shared/src/deepEqual.ts
1496
+ const deepEqual = (left, right) => {
1497
+ if (Object.is(left, right)) return true;
1498
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
1499
+ if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false;
1500
+ if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime();
1501
+ if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((item, index) => deepEqual(item, right[index]));
1502
+ if (!isPlainObject(left) || !isPlainObject(right)) return false;
1503
+ const leftKeys = Object.keys(left);
1504
+ const rightKeys = Object.keys(right);
1505
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && deepEqual(left[key], right[key]));
1506
+ };
1507
+ const isPlainObject = (value) => {
1508
+ const prototype = Object.getPrototypeOf(value);
1509
+ return prototype === Object.prototype || prototype === null;
1510
+ };
1511
+ //#endregion
1512
+ //#region ../js-client-core/src/mountAction.ts
1513
+ const decideMountAction = (client, context) => {
1514
+ if (client.isInitializing) return "none";
1515
+ if (!client.isReady) return "initialize";
1516
+ if (context !== void 0 && !deepEqual(context, client.context)) return "updateContext";
1517
+ return "none";
1518
+ };
1519
+ //#endregion
1433
1520
  //#region src/runtime/app/composables/useConfigDirectorClient.ts
1434
1521
  const useConfigDirectorClient = () => {
1435
1522
  const { $configDirectorClient } = useNuxtApp();
@@ -1451,9 +1538,14 @@ const useConfigDirectorContext = () => {
1451
1538
  state.value = rawContext;
1452
1539
  if (oldContext != newContext) {
1453
1540
  const timeout = getEffectiveTimeout(options?.timeoutMilliseconds);
1454
- await Promise.race([client.updateContext(rawContext), new Promise((resolve) => {
1455
- setTimeout(() => resolve(), timeout);
1456
- })]);
1541
+ let timer;
1542
+ try {
1543
+ await Promise.race([client.updateContext(rawContext), new Promise((resolve) => {
1544
+ timer = setTimeout(() => resolve(), timeout);
1545
+ })]);
1546
+ } finally {
1547
+ clearTimeout(timer);
1548
+ }
1457
1549
  }
1458
1550
  };
1459
1551
  return {
@@ -1470,45 +1562,55 @@ const createDefaultLogger = (level) => {
1470
1562
  };
1471
1563
  //#endregion
1472
1564
  //#region src/runtime/plugin.client.ts
1565
+ const DEFAULT_INITIALIZATION_TIMEOUT_MILLISECONDS = 2e3;
1473
1566
  var plugin_client_default = defineNuxtPlugin((nuxtApp) => {
1474
1567
  const runtimeConfig = useRuntimeConfig();
1475
1568
  const logger = createDefaultLogger(runtimeConfig.public?.configdirector?.logLevel);
1476
- if (!runtimeConfig.public?.configdirector?.clientSdkKey) {
1569
+ const installedTestClient = readInstalledTestClient();
1570
+ const client = installedTestClient ?? buildClient(runtimeConfig.public?.configdirector, logger);
1571
+ logger.debug(installedTestClient ? "Installed ConfigDirector Nuxt plugin with a test client" : "Installed ConfigDirector Nuxt plugin");
1572
+ const readyStatus = shallowRef(client.isReady ? "ready" : "loading");
1573
+ client.on("clientReady", () => {
1574
+ readyStatus.value = "ready";
1575
+ });
1576
+ nuxtApp.hooks.hook("app:created", async () => {
1577
+ const { context } = useConfigDirectorContext();
1578
+ const rawContext = toRaw(context.value);
1579
+ const mountAction = decideMountAction(client, rawContext);
1580
+ logger.debug(`Browser client mount action: ${mountAction}`);
1581
+ if (mountAction === "initialize") await client.initialize(rawContext);
1582
+ else if (mountAction === "updateContext" && rawContext !== void 0) await client.updateContext(rawContext);
1583
+ if (!client.isReady) readyStatus.value = "default";
1584
+ logger.debug(`Browser client mount action awaited, ready status: ${client.isReady}`);
1585
+ });
1586
+ return { provide: {
1587
+ configDirectorClient: client,
1588
+ configDirectorClientReadyStatus: readonly(readyStatus)
1589
+ } };
1590
+ });
1591
+ const buildClient = (configdirector, logger) => {
1592
+ if (!configdirector?.clientSdkKey) {
1477
1593
  const message = "The ConfigDirector clientSdkKey must be configured for the plugin to initialize. You can provide it in nuxt.config.ts under 'runtimeConfig.public.configdirector.clientSdkKey' or as a runtime environment variable named NUXT_PUBLIC_CONFIGDIRECTOR_CLIENT_SDK_KEY";
1478
1594
  logger.error(message);
1479
1595
  throw new ConfigDirectorInitializationError(message);
1480
1596
  }
1481
- logger.debug("Installed ConfigDirector Nuxt plugin");
1482
- const { clientSdkKey, appName, appVersion, baseUrl } = runtimeConfig.public.configdirector;
1483
- const client = createBrowserClient(clientSdkKey, {
1597
+ const { clientSdkKey, appName, appVersion, baseUrl, connection } = configdirector;
1598
+ return createBrowserClient(clientSdkKey, {
1484
1599
  sdkName: "nuxt-sdk",
1485
- sdkVersion: "1.7.0"
1600
+ sdkVersion: "1.8.0"
1486
1601
  }, {
1487
1602
  metadata: {
1488
1603
  appName,
1489
1604
  appVersion
1490
1605
  },
1491
1606
  connection: {
1492
- timeout: 2e3,
1493
- ...baseUrl ? { url: baseUrl } : {}
1607
+ url: baseUrl || void 0,
1608
+ mode: connection?.mode || void 0,
1609
+ pollingInterval: connection?.pollingInterval || void 0,
1610
+ timeout: connection?.timeout || DEFAULT_INITIALIZATION_TIMEOUT_MILLISECONDS
1494
1611
  },
1495
1612
  logger
1496
1613
  });
1497
- const readyStatus = shallowRef(client.isReady ? "ready" : "loading");
1498
- client.on("clientReady", () => {
1499
- readyStatus.value = "ready";
1500
- });
1501
- nuxtApp.hooks.hook("app:created", async () => {
1502
- logger.debug("Initializing browser client");
1503
- const { context } = useConfigDirectorContext();
1504
- await client.initialize(toRaw(context.value));
1505
- if (!client.isReady) readyStatus.value = "default";
1506
- logger.debug(`Browser client initialization awaited, ready status: ${client.isReady}`);
1507
- });
1508
- return { provide: {
1509
- configDirectorClient: client,
1510
- configDirectorClientReadyStatus: readonly(readyStatus)
1511
- } };
1512
- });
1614
+ };
1513
1615
  //#endregion
1514
1616
  export { plugin_client_default as default };
@@ -29,9 +29,14 @@ const useConfigDirectorContext = () => {
29
29
  state.value = rawContext;
30
30
  if (oldContext != newContext) {
31
31
  const timeout = getEffectiveTimeout(options?.timeoutMilliseconds);
32
- await Promise.race([client.updateContext(rawContext), new Promise((resolve) => {
33
- setTimeout(() => resolve(), timeout);
34
- })]);
32
+ let timer;
33
+ try {
34
+ await Promise.race([client.updateContext(rawContext), new Promise((resolve) => {
35
+ timer = setTimeout(() => resolve(), timeout);
36
+ })]);
37
+ } finally {
38
+ clearTimeout(timer);
39
+ }
35
40
  }
36
41
  };
37
42
  return {
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../../types-C54FjC1D.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-Bgx2-3dY.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 { n as ConfigDirectorClient } from "../../types-C54FjC1D.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-Bgx2-3dY.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 ClientHooks } from "../../types-C54FjC1D.mjs";
1
+ import { t as ClientHooks } from "../../types-Bgx2-3dY.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorServerHooks.d.ts
4
4
  declare const useConfigDirectorServerHooks: (hooks: ClientHooks) => void;
@@ -1,4 +1,4 @@
1
- import { t as ClientHooks } from "../../types-C54FjC1D.mjs";
1
+ import { t as ClientHooks } from "../../types-Bgx2-3dY.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorServerHooks.d.ts
4
4
  declare const useConfigDirectorServerHooks: (hooks: ClientHooks) => void;
@@ -4,6 +4,7 @@ import { i as ConfigValueType, n as ConfigEvaluation, r as ConfigState, t as Con
4
4
  type ClientEvents = {
5
5
  configsUpdated: {
6
6
  keys: string[];
7
+ removedKeys: string[];
7
8
  };
8
9
  clientReady: undefined;
9
10
  configEvaluated: {
@@ -23,6 +23,7 @@ type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEv
23
23
  type ClientEvents = {
24
24
  configsUpdated: {
25
25
  keys: string[];
26
+ removedKeys: string[];
26
27
  };
27
28
  clientReady: {
28
29
  action: ClientConnectAction;
@@ -85,8 +86,11 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
85
86
  */
86
87
  get isReady(): boolean;
87
88
  /**
88
- * Returns whether or not the client is currently initializing. Upon client creation, it is `false`.
89
- * It is `true` after calling `initialize` and becomes `false` again once initialization is completed.
89
+ * Whether the client is trying to get its very first payload from the server. Upon client creation,
90
+ * it is `false`. It becomes `true` when `initialize` is called on a client that has never received a
91
+ * payload, stays `true` through timeouts and retries, and becomes `false` when the first payload
92
+ * arrives, when a fatal connection error stops the retries, or on `close`. `updateContext` and
93
+ * `resumeNetwork` never set it, and once a payload has been received it is never `true` again.
90
94
  */
91
95
  get isInitializing(): boolean;
92
96
  /**