@configdirector/nuxt-sdk 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.d.mts CHANGED
@@ -1,6 +1,9 @@
1
1
  import * as _$_nuxt_schema0 from "@nuxt/schema";
2
2
  import { LogLevel } from "consola";
3
3
 
4
+ //#region ../shared/src/types.d.ts
5
+ type ConnectionMode = "streaming" | "polling";
6
+ //#endregion
4
7
  //#region src/module.d.ts
5
8
  interface ConfigDirectorRuntimeConfig {
6
9
  /**
@@ -23,6 +26,40 @@ interface ConfigDirectorRuntimeConfig {
23
26
  * Can be set via the NUXT_CONFIGDIRECTOR_BASE_URL environment variable.
24
27
  */
25
28
  baseUrl?: string;
29
+ /**
30
+ * Connection options for the server SDK client used during SSR and in Nitro handlers.
31
+ */
32
+ connection?: {
33
+ /**
34
+ * The connection mode, one of `streaming` (default) or `polling`. In `streaming` mode the
35
+ * connection stays open and receives config updates as they happen. In `polling` mode configs
36
+ * are fetched once during initialization and then again on every `pollingInterval`.
37
+ * Can be set via the NUXT_CONFIGDIRECTOR_CONNECTION_MODE environment variable.
38
+ */
39
+ mode?: ConnectionMode;
40
+ /**
41
+ * The polling interval in seconds when `mode` is `polling`. Has no effect in `streaming` mode.
42
+ * When omitted (or `0`), the server SDK default is used.
43
+ * Can be set via the NUXT_CONFIGDIRECTOR_CONNECTION_POLLING_INTERVAL environment variable.
44
+ */
45
+ pollingInterval?: number;
46
+ /**
47
+ * How long, in milliseconds, the server SDK client waits for its initial config payload. Requests
48
+ * that arrive before the payload is received are held until it arrives or this timeout elapses,
49
+ * after which they render with default values. When omitted (or `0`), the server SDK default is used.
50
+ * Can be set via the NUXT_CONFIGDIRECTOR_CONNECTION_TIMEOUT environment variable.
51
+ */
52
+ timeout?: number;
53
+ };
54
+ /**
55
+ * Whether requests that arrive before the server SDK client received its initial config payload
56
+ * (for example right after the server starts) wait for it, for up to the connection `timeout`, before
57
+ * being handled. When disabled, such requests are handled immediately and evaluate configs to their
58
+ * default values until the payload arrives.
59
+ *
60
+ * Defaults to `true`. Can be set via the NUXT_CONFIGDIRECTOR_WAIT_FOR_INITIALIZATION environment variable.
61
+ */
62
+ waitForInitialization?: boolean;
26
63
  }
27
64
  interface ConfigDirectorPublicRuntimeConfig {
28
65
  /**
package/dist/module.json CHANGED
@@ -4,5 +4,5 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "1.0.0"
7
+ "version": "1.3.0"
8
8
  }
package/dist/module.mjs CHANGED
@@ -11,14 +11,20 @@ var module_default = defineNuxtModule({
11
11
  setup(options, nuxt) {
12
12
  const logger = useLogger("configdirector");
13
13
  const resolver = createResolver(import.meta.url);
14
- nuxt.options.runtimeConfig.configdirector = defu(nuxt.options.runtimeConfig.configdirector, options.logLevel !== void 0 ? {
15
- serverSdkKey: "",
16
- logLevel: options.logLevel,
17
- baseUrl: ""
18
- } : {
14
+ const serverDefaults = {
19
15
  serverSdkKey: "",
20
- baseUrl: ""
21
- });
16
+ baseUrl: "",
17
+ connection: {
18
+ mode: "streaming",
19
+ pollingInterval: 0,
20
+ timeout: 0
21
+ },
22
+ waitForInitialization: true
23
+ };
24
+ nuxt.options.runtimeConfig.configdirector = defu(nuxt.options.runtimeConfig.configdirector, options.logLevel !== void 0 ? {
25
+ ...serverDefaults,
26
+ logLevel: options.logLevel
27
+ } : serverDefaults);
22
28
  nuxt.options.runtimeConfig.public.configdirector = defu(nuxt.options.runtimeConfig.public.configdirector, options.logLevel !== void 0 ? {
23
29
  clientSdkKey: "",
24
30
  logLevel: options.logLevel,
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../../types-B4EK1B6g.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-BPvGEzJY.mjs";
2
2
 
3
3
  //#region src/runtime/app/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => {
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../../types-B4EK1B6g.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-BPvGEzJY.mjs";
2
2
 
3
3
  //#region src/runtime/app/composables/useConfigDirectorClient.d.ts
4
4
  declare const useConfigDirectorClient: () => {
@@ -1,4 +1,4 @@
1
- import { t as ClientHooks } from "../../types-B4EK1B6g.mjs";
1
+ import { t as ClientHooks } from "../../types-BPvGEzJY.mjs";
2
2
 
3
3
  //#region src/runtime/app/composables/useConfigDirectorClientHooks.d.ts
4
4
  declare const useConfigDirectorClientHooks: (hooks: ClientHooks) => void;
@@ -1,4 +1,4 @@
1
- import { t as ClientHooks } from "../../types-B4EK1B6g.mjs";
1
+ import { t as ClientHooks } from "../../types-BPvGEzJY.mjs";
2
2
 
3
3
  //#region src/runtime/app/composables/useConfigDirectorClientHooks.d.ts
4
4
  declare const useConfigDirectorClientHooks: (hooks: ClientHooks) => void;
@@ -2,7 +2,7 @@ import * as _$h3 from "h3";
2
2
  import { ConfigDirectorClient } from "@configdirector/server-sdk";
3
3
 
4
4
  //#region src/runtime/nitro/middleware.d.ts
5
- declare const _default: _$h3.EventHandler<_$h3.EventHandlerRequest, void>;
5
+ declare const _default: _$h3.EventHandler<_$h3.EventHandlerRequest, Promise<void>>;
6
6
  declare module "h3" {
7
7
  interface H3EventContext {
8
8
  configDirectorClient: ConfigDirectorClient;
@@ -2,7 +2,7 @@ import * as _$h3 from "h3";
2
2
  import { ConfigDirectorClient } from "@configdirector/server-sdk";
3
3
 
4
4
  //#region src/runtime/nitro/middleware.d.ts
5
- declare const _default: _$h3.EventHandler<_$h3.EventHandlerRequest, void>;
5
+ declare const _default: _$h3.EventHandler<_$h3.EventHandlerRequest, Promise<void>>;
6
6
  declare module "h3" {
7
7
  interface H3EventContext {
8
8
  configDirectorClient: ConfigDirectorClient;
@@ -1,8 +1,10 @@
1
- import { useNitroApp } from "nitropack/runtime";
1
+ import { useNitroApp, useRuntimeConfig } from "nitropack/runtime";
2
2
  import { defineEventHandler } from "h3";
3
3
  //#region src/runtime/nitro/middleware.ts
4
- var middleware_default = defineEventHandler((event) => {
5
- event.context.configDirectorClient = useNitroApp().configDirectorClient;
4
+ var middleware_default = defineEventHandler(async (event) => {
5
+ const nitroApp = useNitroApp();
6
+ if (useRuntimeConfig(event).configdirector.waitForInitialization) await nitroApp.configDirectorInitialization;
7
+ event.context.configDirectorClient = nitroApp.configDirectorClient;
6
8
  });
7
9
  //#endregion
8
10
  export { middleware_default as default };
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../types-CzmBIfqo.mjs";
1
+ import { n as ConfigDirectorClient } from "../types-C54FjC1D.mjs";
2
2
  import * as _$nitropack from "nitropack";
3
3
 
4
4
  //#region src/runtime/nitro/plugin.d.ts
@@ -6,6 +6,7 @@ declare const _default: _$nitropack.NitroAppPlugin;
6
6
  declare module "nitropack" {
7
7
  interface NitroApp {
8
8
  configDirectorClient: ConfigDirectorClient;
9
+ configDirectorInitialization: Promise<void>;
9
10
  }
10
11
  }
11
12
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../types-CzmBIfqo.mjs";
1
+ import { n as ConfigDirectorClient } from "../types-C54FjC1D.mjs";
2
2
  import * as _$nitropack from "nitropack";
3
3
 
4
4
  //#region src/runtime/nitro/plugin.d.ts
@@ -6,6 +6,7 @@ declare const _default: _$nitropack.NitroAppPlugin;
6
6
  declare module "nitropack" {
7
7
  interface NitroApp {
8
8
  configDirectorClient: ConfigDirectorClient;
9
+ configDirectorInitialization: Promise<void>;
9
10
  }
10
11
  }
11
12
  //#endregion
@@ -1,8 +1,8 @@
1
1
  import EventEmitter, { EventEmitter as EventEmitter$1 } from "node:events";
2
+ import { createHash, randomUUID } from "node:crypto";
2
3
  import { findByPointer } from "@jsonjoy.com/json-pointer";
3
4
  import * as semver from "semver";
4
5
  import { rapidhash } from "rapidhash-js";
5
- import { createHash } from "node:crypto";
6
6
  import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
7
7
  import { consola } from "consola";
8
8
  //#region ../shared/src/errors.ts
@@ -177,6 +177,7 @@ var EventSourceClient = class {
177
177
  DEFAULT_CALCULATE_RECONNECT_DELAY = () => this.serverReconnectionTime;
178
178
  url;
179
179
  headers;
180
+ body;
180
181
  requestOptions;
181
182
  lastEventId;
182
183
  reconnectAttempt = 0;
@@ -199,8 +200,9 @@ var EventSourceClient = class {
199
200
  Accept: "text/event-stream",
200
201
  ...options.headers
201
202
  };
203
+ const body = options.body;
204
+ this.body = typeof body === "function" ? body : () => body;
202
205
  this.requestOptions = {
203
- body: options.body,
204
206
  method: options.method ?? "GET",
205
207
  mode: options.mode,
206
208
  credentials: options.credentials,
@@ -256,6 +258,7 @@ var EventSourceClient = class {
256
258
  this._readyState = ReadyState.CONNECTING;
257
259
  this._fetch(this.url, {
258
260
  ...this.requestOptions,
261
+ body: this.body(),
259
262
  headers: this.buildRequestHeaders(),
260
263
  signal: this.abortController.signal,
261
264
  cache: "no-store"
@@ -353,18 +356,44 @@ const createEventSourceClient = (options) => {
353
356
  return new EventSourceClient(options);
354
357
  };
355
358
  //#endregion
359
+ //#region ../shared/src/fetchWithTimeout.ts
360
+ const fetchWithTimeout = async (timeout, resource, options, logger) => {
361
+ const abortController = new AbortController();
362
+ const abortTimeoutId = setTimeout(() => {
363
+ logger.debug("[fetchWithTimeout] Reached timeout, aborting request");
364
+ abortController.abort();
365
+ }, timeout);
366
+ try {
367
+ const response = await fetch(typeof resource === "string" || resource instanceof Request ? resource : resource.toString(), {
368
+ ...options,
369
+ signal: abortController.signal
370
+ });
371
+ clearTimeout(abortTimeoutId);
372
+ return response;
373
+ } catch (error) {
374
+ logger.warn("[fetchWithTimeout] Fetch error: ", error);
375
+ clearTimeout(abortTimeoutId);
376
+ throw error;
377
+ }
378
+ };
379
+ //#endregion
356
380
  //#region ../js-server-sdk/src/StreamingTransport.ts
357
381
  const MAX_EXPONENTIAL_DELAY = 9;
382
+ const HEARTBEAT_INTERVAL_MS = 9e4;
358
383
  var StreamingTransport = class {
359
384
  logger;
360
385
  eventSource;
361
386
  eventEmitter = new EventEmitter$1();
362
387
  url;
388
+ heartbeatUrl;
389
+ heartbeatTimer;
390
+ _sessionId;
363
391
  constructor(options) {
364
392
  this.options = options;
365
393
  this.options = options;
366
394
  this.logger = options.logger;
367
395
  this.url = new URL("server/sse/v1", options.baseUrl);
396
+ this.heartbeatUrl = new URL("server/heartbeat/v1", options.baseUrl);
368
397
  }
369
398
  async connect(timeout) {
370
399
  if (this.eventSource) this.close();
@@ -373,10 +402,7 @@ var StreamingTransport = class {
373
402
  url: this.url,
374
403
  method: "POST",
375
404
  headers: { "Content-Type": "application/json" },
376
- body: JSON.stringify({
377
- metaContext: this.options.metaContext,
378
- serverSdkKey: this.options.serverSdkKey
379
- }),
405
+ body: () => this.buildRequestBody(),
380
406
  onMessage: ({ data }) => {
381
407
  this.dispatchMessage(data);
382
408
  },
@@ -386,7 +412,11 @@ var StreamingTransport = class {
386
412
  },
387
413
  shouldReconnect: (state) => {
388
414
  const reconnect = !this.isStatusFatal(state.status);
389
- if (!reconnect) reject(this.prepareFatalError(state.status, state.error));
415
+ if (!reconnect) {
416
+ const error = this.prepareFatalError(state.status, state.error);
417
+ this.eventEmitter.emit("connectionError", error);
418
+ reject(error);
419
+ }
390
420
  return reconnect;
391
421
  },
392
422
  calculateReconnectDelay: (state) => {
@@ -402,10 +432,37 @@ var StreamingTransport = class {
402
432
  });
403
433
  this.eventSource.connect();
404
434
  });
435
+ this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), HEARTBEAT_INTERVAL_MS);
405
436
  return Promise.race([eventSourcePromise, new Promise((resolve) => {
406
437
  setTimeout(() => resolve(this), timeout);
407
438
  })]);
408
439
  }
440
+ get sessionId() {
441
+ return this._sessionId;
442
+ }
443
+ sendHeartbeat() {
444
+ if (!this.isConnected || !this._sessionId) return;
445
+ fetchWithTimeout(HEARTBEAT_INTERVAL_MS, this.heartbeatUrl, {
446
+ method: "POST",
447
+ headers: { "Content-Type": "application/json" },
448
+ body: JSON.stringify({
449
+ serverSdkKey: this.options.serverSdkKey,
450
+ sessionId: this._sessionId
451
+ })
452
+ }, this.logger).then((response) => {
453
+ if (!response.ok) this.logger.debug(`[StreamingTransport] Heartbeat failed with status: ${response.status}`);
454
+ }).catch((error) => {
455
+ this.logger.debug("[StreamingTransport] Heartbeat failed: ", error);
456
+ });
457
+ }
458
+ buildRequestBody() {
459
+ this._sessionId = randomUUID();
460
+ return JSON.stringify({
461
+ metaContext: this.options.metaContext,
462
+ serverSdkKey: this.options.serverSdkKey,
463
+ sessionId: this._sessionId
464
+ });
465
+ }
409
466
  dispatchMessage(data) {
410
467
  try {
411
468
  const json = JSON.parse(data);
@@ -435,6 +492,8 @@ var StreamingTransport = class {
435
492
  this.eventEmitter.removeAllListeners();
436
493
  }
437
494
  close() {
495
+ clearInterval(this.heartbeatTimer);
496
+ this.heartbeatTimer = void 0;
438
497
  this.eventSource?.close();
439
498
  }
440
499
  dispose() {
@@ -459,6 +518,7 @@ var AbstractPollingTransport = class {
459
518
  async handleNonOkResponse(response) {
460
519
  if (!response.ok) if (this.isStatusFatal(response.status)) {
461
520
  this.fatalError = true;
521
+ this.close();
462
522
  throw this.prepareFatalResponseStatusError(response.status, await response.text());
463
523
  } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
464
524
  }
@@ -478,9 +538,7 @@ var AbstractPollingTransport = class {
478
538
  return !!status && status >= 400 && status < 500;
479
539
  }
480
540
  isFetchErrorFatal(fetchError) {
481
- if (fetchError?.name === "NotAllowedError") return true;
482
- else if (fetchError instanceof TypeError) return true;
483
- return false;
541
+ return fetchError?.name === "NotAllowedError";
484
542
  }
485
543
  get isConnected() {
486
544
  return this.pollingInterval !== void 0;
@@ -495,40 +553,20 @@ var AbstractPollingTransport = class {
495
553
  }
496
554
  };
497
555
  //#endregion
498
- //#region ../shared/src/fetchWithTimeout.ts
499
- const fetchWithTimeout = async (timeout, resource, options, logger) => {
500
- const abortController = new AbortController();
501
- const abortTimeoutId = setTimeout(() => {
502
- logger.debug("[fetchWithTimeout] Reached timeout, aborting request");
503
- abortController.abort();
504
- }, timeout);
505
- try {
506
- const response = await fetch(typeof resource === "string" || resource instanceof Request ? resource : resource.toString(), {
507
- ...options,
508
- signal: abortController.signal
509
- });
510
- clearTimeout(abortTimeoutId);
511
- return response;
512
- } catch (error) {
513
- logger.warn("[fetchWithTimeout] Fetch error: ", error);
514
- clearTimeout(abortTimeoutId);
515
- throw error;
516
- }
517
- };
518
- //#endregion
519
556
  //#region ../js-server-sdk/src/PollingTransport.ts
520
557
  var PollingTransport = class extends AbstractPollingTransport {
521
558
  logger;
522
559
  eventEmitter = new EventEmitter$1();
523
560
  url;
524
561
  lastUpdateTimestamp;
562
+ _sessionId = randomUUID();
525
563
  constructor(options) {
526
564
  super();
527
565
  this.options = options;
528
566
  this.options = options;
529
567
  this.logger = options.logger;
530
568
  this.url = new URL("server/polling/v1", options.baseUrl);
531
- this.pollingIntervalSeconds = options.pollingInterval ?? 60;
569
+ this.pollingIntervalSeconds = options.pollingInterval ?? 300;
532
570
  }
533
571
  async connect(timeout) {
534
572
  this.clearPollingInterval();
@@ -543,6 +581,9 @@ var PollingTransport = class extends AbstractPollingTransport {
543
581
  }
544
582
  return this;
545
583
  }
584
+ get sessionId() {
585
+ return this._sessionId;
586
+ }
546
587
  async fetchConfigs(timeout) {
547
588
  if (this.fatalError) {
548
589
  this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
@@ -555,7 +596,8 @@ var PollingTransport = class extends AbstractPollingTransport {
555
596
  body: JSON.stringify({
556
597
  metaContext: this.options.metaContext,
557
598
  serverSdkKey: this.options.serverSdkKey,
558
- lastUpdateTimestamp: this.lastUpdateTimestamp
599
+ lastUpdateTimestamp: this.lastUpdateTimestamp,
600
+ sessionId: this._sessionId
559
601
  })
560
602
  }, this.logger);
561
603
  await this.handleNonOkResponse(response);
@@ -565,7 +607,12 @@ var PollingTransport = class extends AbstractPollingTransport {
565
607
  this.eventEmitter.emit("configBundleReceived", json);
566
608
  }
567
609
  } catch (fetchError) {
568
- this.handleFetchError(fetchError);
610
+ try {
611
+ this.handleFetchError(fetchError);
612
+ } catch (handledError) {
613
+ if (this.fatalError) this.eventEmitter.emit("connectionError", handledError);
614
+ throw handledError;
615
+ }
569
616
  }
570
617
  }
571
618
  on(name, handler) {
@@ -580,14 +627,6 @@ var PollingTransport = class extends AbstractPollingTransport {
580
627
  }
581
628
  };
582
629
  //#endregion
583
- //#region ../js-server-sdk/src/OneTimeTransport.ts
584
- var OneTimeTransport = class extends PollingTransport {
585
- constructor(options) {
586
- super(options);
587
- this.pollingIntervalSeconds = 0;
588
- }
589
- };
590
- //#endregion
591
630
  //#region ../config-evaluator/src/text-comparison.ts
592
631
  /**
593
632
  * Text comparisons.
@@ -649,11 +688,11 @@ const compareNumeric = (givenValue, operator, targetValues) => {
649
688
  const parseFinite = (value) => {
650
689
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
651
690
  if (typeof value !== "string" || value.length === 0) return;
652
- if (!isDecimalCharacters(value)) return;
691
+ if (!isDecimalCharacters$1(value)) return;
653
692
  const parsed = Number(value);
654
693
  return Number.isFinite(parsed) ? parsed : void 0;
655
694
  };
656
- const isDecimalCharacters = (value) => {
695
+ const isDecimalCharacters$1 = (value) => {
657
696
  for (const character of value) if (!(character >= "0" && character <= "9") && !"+-.eE".includes(character)) return false;
658
697
  return true;
659
698
  };
@@ -7088,7 +7127,7 @@ const isNumericNativeType = (requestedType) => {
7088
7127
  const parseConfigValue = (configState, defaultValue) => {
7089
7128
  const value = configState.value;
7090
7129
  const requestedType = getRequestedType(defaultValue);
7091
- if (!value) return {
7130
+ if (value == null) return {
7092
7131
  parsedValue: defaultValue,
7093
7132
  parsedValueId: void 0,
7094
7133
  requestedType,
@@ -7103,7 +7142,7 @@ const parseConfigValue = (configState, defaultValue) => {
7103
7142
  usedDefault: false,
7104
7143
  reason: "found-match"
7105
7144
  };
7106
- if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string")) {
7145
+ if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string" || configState.type === "enum")) {
7107
7146
  const boolValue = parseConfigBoolean(value);
7108
7147
  const hasBoolean = typeof boolValue === "boolean";
7109
7148
  return {
@@ -7137,11 +7176,11 @@ const parseConfigValue = (configState, defaultValue) => {
7137
7176
  };
7138
7177
  }
7139
7178
  return {
7140
- parsedValue: value,
7141
- parsedValueId: configState.valueId,
7179
+ parsedValue: defaultValue,
7180
+ parsedValueId: void 0,
7142
7181
  requestedType,
7143
- usedDefault: false,
7144
- reason: "found-match"
7182
+ usedDefault: true,
7183
+ reason: "type-mismatch"
7145
7184
  };
7146
7185
  };
7147
7186
  const parseConfigBoolean = (value) => {
@@ -7151,17 +7190,22 @@ const parseConfigBoolean = (value) => {
7151
7190
  return lowerValue === "true";
7152
7191
  };
7153
7192
  const parseConfigInteger = (value) => {
7154
- if (!value) return;
7155
- const num = Number.parseInt(value);
7156
- if (isNaN(num) || !isFinite(num)) return;
7157
- return num;
7193
+ const num = parseStrictNumber(value);
7194
+ return num == null ? void 0 : Math.trunc(num);
7158
7195
  };
7159
7196
  const parseConfigFloat = (value) => {
7160
- if (!value) return;
7161
- const num = Number.parseFloat(value);
7197
+ return parseStrictNumber(value);
7198
+ };
7199
+ const parseStrictNumber = (value) => {
7200
+ if (!value || !isDecimalCharacters(value)) return;
7201
+ const num = Number(value);
7162
7202
  if (isNaN(num) || !isFinite(num)) return;
7163
7203
  return num;
7164
7204
  };
7205
+ const isDecimalCharacters = (value) => {
7206
+ for (const character of value) if (!(character >= "0" && character <= "9") && !"+-.eE".includes(character)) return false;
7207
+ return true;
7208
+ };
7165
7209
  const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
7166
7210
  if (typeof defaultValue === "string") return {
7167
7211
  parsedValue: jsonString,
@@ -7171,8 +7215,16 @@ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
7171
7215
  reason: "found-match"
7172
7216
  };
7173
7217
  try {
7218
+ const parsed = JSON.parse(jsonString);
7219
+ if (!isJsonTypeCompatible(parsed, defaultValue)) return {
7220
+ parsedValue: defaultValue,
7221
+ parsedValueId: void 0,
7222
+ requestedType,
7223
+ usedDefault: true,
7224
+ reason: "type-mismatch"
7225
+ };
7174
7226
  return {
7175
- parsedValue: JSON.parse(jsonString),
7227
+ parsedValue: parsed,
7176
7228
  parsedValueId: valueId,
7177
7229
  requestedType,
7178
7230
  usedDefault: false,
@@ -7188,6 +7240,12 @@ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
7188
7240
  };
7189
7241
  }
7190
7242
  };
7243
+ const isJsonTypeCompatible = (parsed, defaultValue) => {
7244
+ if (parsed === null) return false;
7245
+ const defaultType = typeof defaultValue;
7246
+ if (defaultType === "bigint") return typeof parsed === "number";
7247
+ return typeof parsed === defaultType;
7248
+ };
7191
7249
  //#endregion
7192
7250
  //#region ../shared/src/logger.ts
7193
7251
  const LEVELS = {
@@ -7456,11 +7514,25 @@ var TelemetryEventCollector = class {
7456
7514
  this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), initialDelay);
7457
7515
  }
7458
7516
  async flushAndScheduleNext() {
7459
- if ((await this.flush()).fatalError) {
7517
+ if (!this.collectEvents) return;
7518
+ let response;
7519
+ try {
7520
+ response = await this.flush();
7521
+ } catch (error) {
7522
+ this.logger.warn("[TelemetryEventCollector] Error flushing telemetry events: ", error);
7523
+ response = {
7524
+ success: false,
7525
+ fatalError: false
7526
+ };
7527
+ }
7528
+ if (response.fatalError) {
7460
7529
  this.collectEvents = false;
7461
7530
  this.close();
7462
7531
  this.logger.warn("[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.");
7463
- } else this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), this.flushIntervalDelay);
7532
+ } else if (this.collectEvents) {
7533
+ this.cancelScheduledFlush();
7534
+ this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), this.flushIntervalDelay);
7535
+ }
7464
7536
  }
7465
7537
  cancelScheduledFlush() {
7466
7538
  clearTimeout(this.flushTimeout);
@@ -7468,7 +7540,11 @@ var TelemetryEventCollector = class {
7468
7540
  async close() {
7469
7541
  this.collectEvents = false;
7470
7542
  clearTimeout(this.flushTimeout);
7471
- await this.flush();
7543
+ try {
7544
+ await this.flush();
7545
+ } catch (error) {
7546
+ this.logger.warn("[TelemetryEventCollector] Error flushing telemetry events during close: ", error);
7547
+ }
7472
7548
  this.evaluationEventQueue.clear();
7473
7549
  }
7474
7550
  dispose() {
@@ -7681,9 +7757,14 @@ var DefaultConfigDirectorClient = class {
7681
7757
  }
7682
7758
  this.logger.debug(`[ConfigDirectorClient] ConfigSet updated from server with ${configKeys.length} key(s)`, { keys: configKeys });
7683
7759
  });
7760
+ this.transport.on("connectionError", (error) => {
7761
+ this.logger.error("[ConfigDirectorClient] The connection encountered an unrecoverable error and will not be retried. Configs will no longer receive updates.", error);
7762
+ this.eventEmitter.emit("connectionError", { error });
7763
+ });
7684
7764
  this.registerHandler("clientReady", clientOptions?.hooks?.clientReady);
7685
7765
  this.registerHandler("configEvaluated", clientOptions?.hooks?.configEvaluated);
7686
7766
  this.registerHandler("configsUpdated", clientOptions?.hooks?.configsUpdated);
7767
+ this.registerHandler("connectionError", clientOptions?.hooks?.connectionError);
7687
7768
  }
7688
7769
  registerHandler(event, handler) {
7689
7770
  if (!handler) return;
@@ -7717,7 +7798,6 @@ var DefaultConfigDirectorClient = class {
7717
7798
  }
7718
7799
  getTransportConstructor(mode) {
7719
7800
  switch (mode) {
7720
- case "one-time": return OneTimeTransport;
7721
7801
  case "polling": return PollingTransport;
7722
7802
  default: return StreamingTransport;
7723
7803
  }
@@ -7864,6 +7944,9 @@ var DefaultConfigDirectorClient = class {
7864
7944
  dispose() {
7865
7945
  this.removeAllObservers();
7866
7946
  this.closeConnection();
7947
+ this.usageEventCollector.close().catch((error) => {
7948
+ this.logger.warn("[ConfigDirectorClient] Error flushing telemetry while disposing the client: ", error);
7949
+ });
7867
7950
  }
7868
7951
  };
7869
7952
  //#endregion
@@ -7884,16 +7967,21 @@ var plugin_default = defineNitroPlugin((nitroApp) => {
7884
7967
  throw new ConfigDirectorInitializationError(message);
7885
7968
  }
7886
7969
  logger.debug("Initializing ConfigDirector Nitro plugin");
7887
- const { serverSdkKey, baseUrl } = runtimeConfig.configdirector;
7970
+ const { serverSdkKey, baseUrl, connection } = runtimeConfig.configdirector;
7888
7971
  const client = new DefaultConfigDirectorClient(serverSdkKey, {
7889
7972
  sdkName: "nuxt-sdk",
7890
- sdkVersion: "1.0.0"
7973
+ sdkVersion: "1.3.0"
7891
7974
  }, {
7892
7975
  logger,
7893
- ...baseUrl ? { connection: { url: baseUrl } } : {}
7976
+ connection: {
7977
+ url: baseUrl || void 0,
7978
+ mode: connection?.mode || void 0,
7979
+ pollingInterval: connection?.pollingInterval || void 0,
7980
+ timeout: connection?.timeout || void 0
7981
+ }
7894
7982
  });
7895
7983
  nitroApp.configDirectorClient = client;
7896
- client.initialize();
7984
+ nitroApp.configDirectorInitialization = client.initialize();
7897
7985
  });
7898
7986
  //#endregion
7899
7987
  export { plugin_default as default };
@@ -207,6 +207,7 @@ var EventSourceClient = class {
207
207
  DEFAULT_CALCULATE_RECONNECT_DELAY = () => this.serverReconnectionTime;
208
208
  url;
209
209
  headers;
210
+ body;
210
211
  requestOptions;
211
212
  lastEventId;
212
213
  reconnectAttempt = 0;
@@ -229,8 +230,9 @@ var EventSourceClient = class {
229
230
  Accept: "text/event-stream",
230
231
  ...options.headers
231
232
  };
233
+ const body = options.body;
234
+ this.body = typeof body === "function" ? body : () => body;
232
235
  this.requestOptions = {
233
- body: options.body,
234
236
  method: options.method ?? "GET",
235
237
  mode: options.mode,
236
238
  credentials: options.credentials,
@@ -286,6 +288,7 @@ var EventSourceClient = class {
286
288
  this._readyState = ReadyState.CONNECTING;
287
289
  this._fetch(this.url, {
288
290
  ...this.requestOptions,
291
+ body: this.body(),
289
292
  headers: this.buildRequestHeaders(),
290
293
  signal: this.abortController.signal,
291
294
  cache: "no-store"
@@ -419,7 +422,11 @@ var StreamingTransport = class {
419
422
  },
420
423
  shouldReconnect: (state) => {
421
424
  const reconnect = !this.isStatusFatal(state.status);
422
- if (!reconnect) reject(this.prepareFatalError(state.status, state.error));
425
+ if (!reconnect) {
426
+ const error = this.prepareFatalError(state.status, state.error);
427
+ this.eventEmitter.emit("connectionError", error);
428
+ reject(error);
429
+ }
423
430
  return reconnect;
424
431
  },
425
432
  calculateReconnectDelay: (state) => {
@@ -500,7 +507,7 @@ const isNumericNativeType = (requestedType) => {
500
507
  const parseConfigValue = (configState, defaultValue) => {
501
508
  const value = configState.value;
502
509
  const requestedType = getRequestedType(defaultValue);
503
- if (!value) return {
510
+ if (value == null) return {
504
511
  parsedValue: defaultValue,
505
512
  parsedValueId: void 0,
506
513
  requestedType,
@@ -515,7 +522,7 @@ const parseConfigValue = (configState, defaultValue) => {
515
522
  usedDefault: false,
516
523
  reason: "found-match"
517
524
  };
518
- if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string")) {
525
+ if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string" || configState.type === "enum")) {
519
526
  const boolValue = parseConfigBoolean(value);
520
527
  const hasBoolean = typeof boolValue === "boolean";
521
528
  return {
@@ -549,11 +556,11 @@ const parseConfigValue = (configState, defaultValue) => {
549
556
  };
550
557
  }
551
558
  return {
552
- parsedValue: value,
553
- parsedValueId: configState.valueId,
559
+ parsedValue: defaultValue,
560
+ parsedValueId: void 0,
554
561
  requestedType,
555
- usedDefault: false,
556
- reason: "found-match"
562
+ usedDefault: true,
563
+ reason: "type-mismatch"
557
564
  };
558
565
  };
559
566
  const parseConfigBoolean = (value) => {
@@ -563,17 +570,22 @@ const parseConfigBoolean = (value) => {
563
570
  return lowerValue === "true";
564
571
  };
565
572
  const parseConfigInteger = (value) => {
566
- if (!value) return;
567
- const num = Number.parseInt(value);
568
- if (isNaN(num) || !isFinite(num)) return;
569
- return num;
573
+ const num = parseStrictNumber(value);
574
+ return num == null ? void 0 : Math.trunc(num);
570
575
  };
571
576
  const parseConfigFloat = (value) => {
572
- if (!value) return;
573
- const num = Number.parseFloat(value);
577
+ return parseStrictNumber(value);
578
+ };
579
+ const parseStrictNumber = (value) => {
580
+ if (!value || !isDecimalCharacters(value)) return;
581
+ const num = Number(value);
574
582
  if (isNaN(num) || !isFinite(num)) return;
575
583
  return num;
576
584
  };
585
+ const isDecimalCharacters = (value) => {
586
+ for (const character of value) if (!(character >= "0" && character <= "9") && !"+-.eE".includes(character)) return false;
587
+ return true;
588
+ };
577
589
  const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
578
590
  if (typeof defaultValue === "string") return {
579
591
  parsedValue: jsonString,
@@ -583,8 +595,16 @@ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
583
595
  reason: "found-match"
584
596
  };
585
597
  try {
598
+ const parsed = JSON.parse(jsonString);
599
+ if (!isJsonTypeCompatible(parsed, defaultValue)) return {
600
+ parsedValue: defaultValue,
601
+ parsedValueId: void 0,
602
+ requestedType,
603
+ usedDefault: true,
604
+ reason: "type-mismatch"
605
+ };
586
606
  return {
587
- parsedValue: JSON.parse(jsonString),
607
+ parsedValue: parsed,
588
608
  parsedValueId: valueId,
589
609
  requestedType,
590
610
  usedDefault: false,
@@ -600,6 +620,12 @@ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
600
620
  };
601
621
  }
602
622
  };
623
+ const isJsonTypeCompatible = (parsed, defaultValue) => {
624
+ if (parsed === null) return false;
625
+ const defaultType = typeof defaultValue;
626
+ if (defaultType === "bigint") return typeof parsed === "number";
627
+ return typeof parsed === defaultType;
628
+ };
603
629
  //#endregion
604
630
  //#region ../shared/src/logger.ts
605
631
  const LEVELS = {
@@ -664,6 +690,111 @@ const createDefaultLogger$1 = (level, messageDecorator) => {
664
690
  return new DefaultConsoleLogger(level ?? "warn", messageDecorator ?? new LogMessageDecorator());
665
691
  };
666
692
  //#endregion
693
+ //#region ../shared/src/url.ts
694
+ const defaultUrlFactory = (input, base) => new URL(input, base?.toString());
695
+ const parseUrl = (url) => {
696
+ if (!url) return;
697
+ try {
698
+ return new URL(url);
699
+ } catch (error) {
700
+ throw new ConfigDirectorValidationError(`Invalid URL '${url}'. Parsing failed: ${error}`);
701
+ }
702
+ };
703
+ //#endregion
704
+ //#region ../shared/src/instance-id.ts
705
+ const generateInstanceId = () => {
706
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
707
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (placeholder) => {
708
+ const random = Math.random() * 16 | 0;
709
+ return (placeholder === "x" ? random : random & 3 | 8).toString(16);
710
+ });
711
+ };
712
+ //#endregion
713
+ //#region ../js-client-core/src/instance-id.ts
714
+ const STORAGE_KEY = "configdirector-sdk:instance-id";
715
+ const RECORD_VERSION = 1;
716
+ const MAX_AGE_MS = 360 * 60 * 1e3;
717
+ const CHECKSUM_SALT = "configdirector.instanceId.v1";
718
+ const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
719
+ const fnv1a = (input, offsetBasis) => {
720
+ let hash = offsetBasis;
721
+ for (let i = 0; i < input.length; i++) {
722
+ hash ^= input.charCodeAt(i);
723
+ hash = Math.imul(hash, 16777619);
724
+ }
725
+ return hash >>> 0;
726
+ };
727
+ const checksumFor = (id, createdAt) => {
728
+ const payload = `${CHECKSUM_SALT}|${id}|${createdAt}`;
729
+ const low = fnv1a(payload, 2166136261);
730
+ return fnv1a(`${low}|${payload}`, 16777619).toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
731
+ };
732
+ const getLocalStorage = () => {
733
+ try {
734
+ return globalThis.localStorage ?? void 0;
735
+ } catch {
736
+ return;
737
+ }
738
+ };
739
+ const isUsableRecord = (record) => {
740
+ if (!record || typeof record !== "object") return false;
741
+ const { v, id, createdAt, checksum } = record;
742
+ if (v !== RECORD_VERSION || typeof id !== "string" || typeof checksum !== "string") return false;
743
+ if (typeof createdAt !== "number" || !Number.isInteger(createdAt) || createdAt <= 0) return false;
744
+ if (!UUID_V4_PATTERN.test(id)) return false;
745
+ return checksum === checksumFor(id, createdAt);
746
+ };
747
+ const readStoredInstanceId = (storage, logger) => {
748
+ let raw;
749
+ try {
750
+ raw = storage.getItem(STORAGE_KEY);
751
+ } catch (error) {
752
+ logger?.debug("[ConfigDirectorClient] Could not read the stored instance id, generating a new one", error);
753
+ return;
754
+ }
755
+ if (!raw) return;
756
+ let record;
757
+ try {
758
+ record = JSON.parse(raw);
759
+ } catch {
760
+ logger?.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
761
+ return;
762
+ }
763
+ if (!isUsableRecord(record)) {
764
+ logger?.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
765
+ return;
766
+ }
767
+ const age = Date.now() - record.createdAt;
768
+ if (age < 0 || age >= MAX_AGE_MS) {
769
+ logger?.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
770
+ return;
771
+ }
772
+ return record.id;
773
+ };
774
+ const storeInstanceId = (storage, id, logger) => {
775
+ const createdAt = Date.now();
776
+ const record = {
777
+ v: RECORD_VERSION,
778
+ id,
779
+ createdAt,
780
+ checksum: checksumFor(id, createdAt)
781
+ };
782
+ try {
783
+ storage.setItem(STORAGE_KEY, JSON.stringify(record));
784
+ } catch (error) {
785
+ logger?.debug("[ConfigDirectorClient] Could not persist the instance id", error);
786
+ }
787
+ };
788
+ const resolveInstanceId = (logger) => {
789
+ const storage = getLocalStorage();
790
+ if (!storage) return generateInstanceId();
791
+ const storedInstanceId = readStoredInstanceId(storage, logger);
792
+ if (storedInstanceId) return storedInstanceId;
793
+ const instanceId = generateInstanceId();
794
+ storeInstanceId(storage, instanceId, logger);
795
+ return instanceId;
796
+ };
797
+ //#endregion
667
798
  //#region ../shared/src/transport/AbstractPollingTransport.ts
668
799
  var AbstractPollingTransport = class {
669
800
  fatalError = false;
@@ -680,6 +811,7 @@ var AbstractPollingTransport = class {
680
811
  async handleNonOkResponse(response) {
681
812
  if (!response.ok) if (this.isStatusFatal(response.status)) {
682
813
  this.fatalError = true;
814
+ this.close();
683
815
  throw this.prepareFatalResponseStatusError(response.status, await response.text());
684
816
  } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
685
817
  }
@@ -699,9 +831,7 @@ var AbstractPollingTransport = class {
699
831
  return !!status && status >= 400 && status < 500;
700
832
  }
701
833
  isFetchErrorFatal(fetchError) {
702
- if (fetchError?.name === "NotAllowedError") return true;
703
- else if (fetchError instanceof TypeError) return true;
704
- return false;
834
+ return fetchError?.name === "NotAllowedError";
705
835
  }
706
836
  get isConnected() {
707
837
  return this.pollingInterval !== void 0;
@@ -749,7 +879,9 @@ var PollingTransport = class extends AbstractPollingTransport {
749
879
  this.options = options;
750
880
  this.logger = options.logger;
751
881
  this.url = options.resolveUrl("client/polling/v1", options.baseUrl);
752
- this.pollingIntervalSeconds = options.pollingInterval ?? 60;
882
+ const requestedInterval = options.pollingInterval ?? 300;
883
+ if (requestedInterval < 60) this.logger.warn(`[PollingTransport] pollingInterval of ${requestedInterval} seconds is below the minimum of 60 seconds. Using 60 seconds.`);
884
+ this.pollingIntervalSeconds = Math.max(requestedInterval, 60);
753
885
  }
754
886
  async connect(context, timeout) {
755
887
  this.clearPollingInterval();
@@ -788,7 +920,12 @@ var PollingTransport = class extends AbstractPollingTransport {
788
920
  this.eventEmitter.emit("configSetReceived", json);
789
921
  }
790
922
  } catch (fetchError) {
791
- this.handleFetchError(fetchError);
923
+ try {
924
+ this.handleFetchError(fetchError);
925
+ } catch (handledError) {
926
+ if (this.fatalError) this.eventEmitter.emit("connectionError", handledError);
927
+ throw handledError;
928
+ }
792
929
  }
793
930
  }
794
931
  on(name, handler) {
@@ -802,33 +939,16 @@ var PollingTransport = class extends AbstractPollingTransport {
802
939
  }
803
940
  };
804
941
  //#endregion
805
- //#region ../js-client-core/src/OneTimeTransport.ts
806
- var OneTimeTransport = class extends PollingTransport {
807
- constructor(options) {
808
- super(options);
809
- this.pollingIntervalSeconds = 0;
810
- }
811
- };
812
- //#endregion
813
- //#region ../shared/src/url.ts
814
- const defaultUrlFactory = (input, base) => new URL(input, base?.toString());
815
- const parseUrl = (url) => {
816
- if (!url) return;
942
+ //#region ../js-client-core/src/browser-globals.ts
943
+ const read = (readValue) => {
817
944
  try {
818
- return new URL(url);
819
- } catch (error) {
820
- throw new ConfigDirectorValidationError(`Invalid URL '${url}'. Parsing failed: ${error}`);
945
+ return readValue(globalThis);
946
+ } catch {
947
+ return;
821
948
  }
822
949
  };
823
- //#endregion
824
- //#region ../shared/src/instance-id.ts
825
- const generateInstanceId = () => {
826
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
827
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (placeholder) => {
828
- const random = Math.random() * 16 | 0;
829
- return (placeholder === "x" ? random : random & 3 | 8).toString(16);
830
- });
831
- };
950
+ const readUserAgent = () => read((globals) => globals.navigator?.userAgent);
951
+ const readHost = () => read((globals) => globals.location?.host);
832
952
  //#endregion
833
953
  //#region ../js-client-core/src/DefaultConfigDirectorClient.ts
834
954
  const MAX_EXPONENTIAL_DELAY = 9;
@@ -849,8 +969,8 @@ var DefaultConfigDirectorClient = class {
849
969
  connectionMode;
850
970
  instanceId;
851
971
  constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
852
- this.instanceId = generateInstanceId();
853
972
  this.logger = clientOptions?.logger ?? createDefaultLogger$1();
973
+ this.instanceId = resolveInstanceId(this.logger);
854
974
  this.timeout = clientOptions?.connection?.timeout ?? 3e3;
855
975
  this.validateSdkKeyPresence(clientSdkKey);
856
976
  const urlFactory = internalClientOptions?.urlFactory ?? defaultUrlFactory;
@@ -867,7 +987,8 @@ var DefaultConfigDirectorClient = class {
867
987
  ...clientOptions?.metadata,
868
988
  sdkName: sdkOptions.sdkName,
869
989
  sdkVersion: sdkOptions.sdkVersion,
870
- userAgent: navigator?.userAgent
990
+ userAgent: readUserAgent(),
991
+ host: readHost()
871
992
  },
872
993
  logger: this.logger,
873
994
  fetch: internalClientOptions?.fetch,
@@ -893,10 +1014,15 @@ var DefaultConfigDirectorClient = class {
893
1014
  }
894
1015
  this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", { keys: configKeys });
895
1016
  });
1017
+ this.transport.on("connectionError", (error) => {
1018
+ this.logger.error("[ConfigDirectorClient] The connection encountered an unrecoverable error and will not be retried. Configs will no longer receive updates.", error);
1019
+ this.emit("connectionError", { error });
1020
+ });
896
1021
  this.registerHandler("clientReady", clientOptions?.hooks?.clientReady);
897
1022
  this.registerHandler("configEvaluated", clientOptions?.hooks?.configEvaluated);
898
1023
  this.registerHandler("configsUpdated", clientOptions?.hooks?.configsUpdated);
899
1024
  this.registerHandler("contextUpdated", clientOptions?.hooks?.contextUpdated);
1025
+ this.registerHandler("connectionError", clientOptions?.hooks?.connectionError);
900
1026
  }
901
1027
  registerHandler(event, handler) {
902
1028
  if (!handler) return;
@@ -905,7 +1031,6 @@ var DefaultConfigDirectorClient = class {
905
1031
  }
906
1032
  getTransportConstructor(connectionMode) {
907
1033
  switch (connectionMode) {
908
- case "one-time": return OneTimeTransport;
909
1034
  case "polling": return PollingTransport;
910
1035
  default: return StreamingTransport;
911
1036
  }
@@ -925,7 +1050,8 @@ var DefaultConfigDirectorClient = class {
925
1050
  this.ready = false;
926
1051
  this.readyPromise = new Promise((resolve) => {
927
1052
  this.readyResolve = resolve;
928
- }).then(() => {
1053
+ }).then((cancelled) => {
1054
+ if (cancelled) return;
929
1055
  this.ready = true;
930
1056
  this.initializing = false;
931
1057
  this.emit("clientReady", { action: caller });
@@ -1085,10 +1211,11 @@ var DefaultConfigDirectorClient = class {
1085
1211
  async close() {
1086
1212
  this.logger.debug("[ConfigDirectorClient] close() has been called, closing connection to server");
1087
1213
  clearTimeout(this.timeoutTimer);
1088
- this.readyResolve?.();
1214
+ this.readyResolve?.(true);
1089
1215
  this.telemetryClient.close();
1090
1216
  this.transport.close();
1091
1217
  this.ready = false;
1218
+ this.initializing = false;
1092
1219
  }
1093
1220
  dispose() {
1094
1221
  this.clear();
@@ -1122,14 +1249,18 @@ const mapToTelemetryValue = (ev) => {
1122
1249
  };
1123
1250
  //#endregion
1124
1251
  //#region ../js-browser-client/src/telemetry/WebWorkerTelemetryClient.ts
1252
+ const DEFAULT_WORKER_CLOSE_TIMEOUT = 6e3;
1125
1253
  var WebWorkerTelemetryClient = class {
1126
1254
  logger;
1127
1255
  worker;
1128
1256
  closePromise;
1129
1257
  closeResolve;
1258
+ closeTimer;
1259
+ workerCloseTimeout;
1130
1260
  visibilityHandler;
1131
1261
  constructor(options) {
1132
1262
  this.logger = options.logger;
1263
+ this.workerCloseTimeout = options.workerCloseTimeout ?? DEFAULT_WORKER_CLOSE_TIMEOUT;
1133
1264
  const initializeMessage = {
1134
1265
  type: "Initialize",
1135
1266
  payload: {
@@ -1141,7 +1272,7 @@ var WebWorkerTelemetryClient = class {
1141
1272
  flushIntervalDelay: options.flushIntervalDelay ?? 3e4
1142
1273
  }
1143
1274
  };
1144
- 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.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},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}},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(){(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)}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout),await this.flush(),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=>{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`: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" })));
1275
+ 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.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},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}},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" })));
1145
1276
  this.worker.onmessage = (message) => {
1146
1277
  this.handleWorkerEvent(message.data);
1147
1278
  };
@@ -1185,7 +1316,7 @@ var WebWorkerTelemetryClient = class {
1185
1316
  }
1186
1317
  handleWorkerEvent(event) {
1187
1318
  if (event.type === "Closed") {
1188
- this.closeResolve?.();
1319
+ this.finishClose();
1189
1320
  return;
1190
1321
  }
1191
1322
  if (!event.payload?.level || !event.payload?.message) return;
@@ -1201,7 +1332,7 @@ var WebWorkerTelemetryClient = class {
1201
1332
  this.logger.warn(message, ...args);
1202
1333
  break;
1203
1334
  case "error":
1204
- this.logger.warn(message, ...args);
1335
+ this.logger.error(message, ...args);
1205
1336
  break;
1206
1337
  }
1207
1338
  }
@@ -1213,13 +1344,27 @@ var WebWorkerTelemetryClient = class {
1213
1344
  return this.performClose();
1214
1345
  }
1215
1346
  performClose() {
1216
- document.removeEventListener("visibilitychange", this.visibilityHandler);
1347
+ try {
1348
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1349
+ } catch (error) {
1350
+ this.logger.warn("[WebWorkerTelemetryClient] Could not remove 'visibilitychange' listener: ", error);
1351
+ }
1217
1352
  this.closePromise = new Promise((resolve) => {
1218
1353
  this.closeResolve = resolve;
1354
+ this.closeTimer = setTimeout(() => {
1355
+ this.logger.warn("[WebWorkerTelemetryClient] Timed out waiting for the telemetry worker to close. Terminating the worker.");
1356
+ this.finishClose();
1357
+ }, this.workerCloseTimeout);
1219
1358
  this.worker.postMessage({ type: "Close" });
1220
1359
  });
1221
1360
  return this.closePromise;
1222
1361
  }
1362
+ finishClose() {
1363
+ clearTimeout(this.closeTimer);
1364
+ this.closeTimer = void 0;
1365
+ this.worker.terminate();
1366
+ this.closeResolve?.();
1367
+ }
1223
1368
  };
1224
1369
  //#endregion
1225
1370
  //#region ../shared/src/base62-encoder.ts
@@ -1241,6 +1386,7 @@ const BASE62_LENGTH = Math.ceil(128 / Math.log2(62));
1241
1386
  //#region ../js-browser-client/src/telemetry/value-id-generator.ts
1242
1387
  const encoder = new TextEncoder();
1243
1388
  const generateValueId = async (v) => {
1389
+ if (typeof crypto === "undefined" || crypto.subtle == null) return;
1244
1390
  const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(v?.toString() ?? ""));
1245
1391
  return toBase62(new Uint8Array(hashBuffer, 0, 16), BASE62_LENGTH);
1246
1392
  };
@@ -1317,7 +1463,7 @@ var plugin_client_default = defineNuxtPlugin((nuxtApp) => {
1317
1463
  const { clientSdkKey, appName, appVersion, baseUrl } = runtimeConfig.public.configdirector;
1318
1464
  const client = createBrowserClient(clientSdkKey, {
1319
1465
  sdkName: "nuxt-sdk",
1320
- sdkVersion: "1.0.0"
1466
+ sdkVersion: "1.3.0"
1321
1467
  }, {
1322
1468
  metadata: {
1323
1469
  appName,
@@ -83,15 +83,11 @@ var SsrClient = class {
83
83
  getValue(configKey, defaultValue) {
84
84
  return this.serverClient.getValue(configKey, defaultValue, this.currentContext);
85
85
  }
86
- watch(configKey, defaultValue, callback) {
87
- return this.serverClient.watch(configKey, defaultValue, callback, this.currentContext);
88
- }
89
- unwatch(configKey, callback) {
90
- return this.serverClient.unwatch(configKey, callback);
91
- }
92
- unwatchAll() {
93
- return this.serverClient.unwatchAll();
86
+ watch() {
87
+ return () => {};
94
88
  }
89
+ unwatch() {}
90
+ unwatchAll() {}
95
91
  pauseNetwork() {}
96
92
  async resumeNetwork() {}
97
93
  dispose() {}
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../../types-CzmBIfqo.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-C54FjC1D.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-CzmBIfqo.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-C54FjC1D.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-CzmBIfqo.mjs";
1
+ import { t as ClientHooks } from "../../types-C54FjC1D.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-CzmBIfqo.mjs";
1
+ import { t as ClientHooks } from "../../types-C54FjC1D.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorServerHooks.d.ts
4
4
  declare const useConfigDirectorServerHooks: (hooks: ClientHooks) => void;
@@ -16,6 +16,7 @@ type ClientHooks = {
16
16
  configsUpdated?: HookHandler<"configsUpdated"> | HookHandler<"configsUpdated">[];
17
17
  contextUpdated?: HookHandler<"contextUpdated"> | HookHandler<"contextUpdated">[];
18
18
  configEvaluated?: HookHandler<"configEvaluated"> | HookHandler<"configEvaluated">[];
19
+ connectionError?: HookHandler<"connectionError"> | HookHandler<"connectionError">[];
19
20
  };
20
21
  type ClientConnectAction = "initialization" | "context update" | "network resume";
21
22
  type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEvent]) => void;
@@ -32,6 +33,9 @@ type ClientEvents = {
32
33
  configEvaluated: {
33
34
  evaluation: ConfigEvaluation;
34
35
  };
36
+ connectionError: {
37
+ error: Error;
38
+ };
35
39
  };
36
40
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
37
41
  /**
@@ -9,11 +9,15 @@ type ClientEvents = {
9
9
  configEvaluated: {
10
10
  evaluation: ConfigEvaluation;
11
11
  };
12
+ connectionError: {
13
+ error: Error;
14
+ };
12
15
  };
13
16
  type ClientHooks = {
14
17
  clientReady?: HookHandler<"clientReady"> | HookHandler<"clientReady">[];
15
18
  configsUpdated?: HookHandler<"configsUpdated"> | HookHandler<"configsUpdated">[];
16
19
  configEvaluated?: HookHandler<"configEvaluated"> | HookHandler<"configEvaluated">[];
20
+ connectionError?: HookHandler<"connectionError"> | HookHandler<"connectionError">[];
17
21
  };
18
22
  type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEvent]) => void;
19
23
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/nuxt-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.3.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",
@@ -58,14 +58,14 @@
58
58
  "semver": "^7.7.4"
59
59
  },
60
60
  "devDependencies": {
61
- "@nuxt/devtools": "^3.2.4",
61
+ "@nuxt/devtools": "^3.4.2",
62
62
  "@nuxt/eslint-config": "^1.15.2",
63
63
  "@nuxt/schema": "4.4.8",
64
64
  "@nuxt/test-utils": "^4.0.2",
65
65
  "@types/node": "^24.13.3",
66
66
  "changelogen": "^0.6.2",
67
67
  "eslint": "^10.7.0",
68
- "nuxt": "^4.4.8",
68
+ "nuxt": "^4.5.2",
69
69
  "playwright": "^1.60.0",
70
70
  "rimraf": "^6.1.3",
71
71
  "tsdown": "^0.21.7",