@truenas/api-client 2.0.0 → 2.0.1

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/index.d.cts CHANGED
@@ -27443,7 +27443,11 @@ type DefaultApiDirectory = ApiDirectory$7;
27443
27443
  interface CreateClientOptions {
27444
27444
  /** System UUID. */
27445
27445
  uuid: string;
27446
- /** Hostnames to connect to — primary first, then fallbacks. */
27446
+ /**
27447
+ * Hostnames to connect to. Order carries no precedence for version discovery
27448
+ * (every hostname is asked at once); it only breaks ties when deciding which
27449
+ * failure to report if none of them answer.
27450
+ */
27447
27451
  hostnames: string[];
27448
27452
  /**
27449
27453
  * Initial connection gate. The client only opens a socket while this is `true`;
@@ -27462,7 +27466,8 @@ interface CreateClientOptions {
27462
27466
  /**
27463
27467
  * Creates a version-specific TrueNAS API client.
27464
27468
  *
27465
- * 1. Discovers the API version from the primary hostname (`GET /api/versions`).
27469
+ * 1. Discovers the API version (`GET /api/versions`), asking every hostname in
27470
+ * parallel. The first usable answer wins.
27466
27471
  * 2. Selects the matching client implementation (`v25.10.x` -> `TrueNasApiClientV2510`,
27467
27472
  * `v26.x.y` -> `TrueNasApiClientV26`).
27468
27473
  * 3. Instantiates and returns it.
@@ -27498,7 +27503,12 @@ interface CreateClientOptions {
27498
27503
  * it, so naming a method this surface does not have is a build error.
27499
27504
  * @returns a Promise that resolves with the created client, or rejects with a
27500
27505
  * {@link VersionDiscoveryError} subclass (or a client-selection error).
27501
- * Rejects if `hostnames` is empty.
27506
+ * Rejects if version discovery on all hostnames *fails* and is not recoverable.
27507
+ * Note that when the selected failure is a `VersionDiscoveryNetworkError`,
27508
+ * this function attempts to use a fallback API version (see `FALLBACK_VERSION`)
27509
+ * because network errors are actually expected on 25.10.0 systems due to a CORS
27510
+ * bug. A network error alongside a version-compatibility error or a 404 does
27511
+ * *not* reach the fallback — see `selectRepresentativeFailure`.
27502
27512
  */
27503
27513
  declare function createTrueNasClient<D extends ApiDirectoryShape = DefaultApiDirectory>(opts: CreateClientOptions): Promise<TrueNasApiClient<D>>;
27504
27514
 
package/dist/index.d.ts CHANGED
@@ -27443,7 +27443,11 @@ type DefaultApiDirectory = ApiDirectory$7;
27443
27443
  interface CreateClientOptions {
27444
27444
  /** System UUID. */
27445
27445
  uuid: string;
27446
- /** Hostnames to connect to — primary first, then fallbacks. */
27446
+ /**
27447
+ * Hostnames to connect to. Order carries no precedence for version discovery
27448
+ * (every hostname is asked at once); it only breaks ties when deciding which
27449
+ * failure to report if none of them answer.
27450
+ */
27447
27451
  hostnames: string[];
27448
27452
  /**
27449
27453
  * Initial connection gate. The client only opens a socket while this is `true`;
@@ -27462,7 +27466,8 @@ interface CreateClientOptions {
27462
27466
  /**
27463
27467
  * Creates a version-specific TrueNAS API client.
27464
27468
  *
27465
- * 1. Discovers the API version from the primary hostname (`GET /api/versions`).
27469
+ * 1. Discovers the API version (`GET /api/versions`), asking every hostname in
27470
+ * parallel. The first usable answer wins.
27466
27471
  * 2. Selects the matching client implementation (`v25.10.x` -> `TrueNasApiClientV2510`,
27467
27472
  * `v26.x.y` -> `TrueNasApiClientV26`).
27468
27473
  * 3. Instantiates and returns it.
@@ -27498,7 +27503,12 @@ interface CreateClientOptions {
27498
27503
  * it, so naming a method this surface does not have is a build error.
27499
27504
  * @returns a Promise that resolves with the created client, or rejects with a
27500
27505
  * {@link VersionDiscoveryError} subclass (or a client-selection error).
27501
- * Rejects if `hostnames` is empty.
27506
+ * Rejects if version discovery on all hostnames *fails* and is not recoverable.
27507
+ * Note that when the selected failure is a `VersionDiscoveryNetworkError`,
27508
+ * this function attempts to use a fallback API version (see `FALLBACK_VERSION`)
27509
+ * because network errors are actually expected on 25.10.0 systems due to a CORS
27510
+ * bug. A network error alongside a version-compatibility error or a 404 does
27511
+ * *not* reach the fallback — see `selectRepresentativeFailure`.
27502
27512
  */
27503
27513
  declare function createTrueNasClient<D extends ApiDirectoryShape = DefaultApiDirectory>(opts: CreateClientOptions): Promise<TrueNasApiClient<D>>;
27504
27514
 
package/dist/index.js CHANGED
@@ -3304,7 +3304,7 @@ var VersionDiscovery = class {
3304
3304
  map$1((versionStrings) => this.selectVersion(hostname, versionStrings)),
3305
3305
  catchError((error) => {
3306
3306
  this.versionCache.delete(hostname);
3307
- this.logger.error("Version discovery failed", { hostname, error });
3307
+ this.logger.warn("Version discovery failed", { hostname, error });
3308
3308
  return throwError(() => this.classify(error, hostname));
3309
3309
  }),
3310
3310
  shareReplay(1)
@@ -3375,7 +3375,7 @@ var VersionDiscovery = class {
3375
3375
  });
3376
3376
  const parsedVersions = versionStrings.map(parseApiVersion).filter((v) => v !== null);
3377
3377
  if (parsedVersions.length === 0) {
3378
- this.logger.error("No valid versions in response", {
3378
+ this.logger.warn("No valid versions in response", {
3379
3379
  hostname,
3380
3380
  versionStrings
3381
3381
  });
@@ -3477,29 +3477,34 @@ async function createTrueNasClient(opts) {
3477
3477
  `Cannot create client for system ${uuid}: hostnames array is empty`
3478
3478
  );
3479
3479
  }
3480
- const primaryHostname = hostnames[0];
3481
3480
  const versionDiscovery = new VersionDiscovery(logger);
3482
3481
  logger.info("Creating versioned API client", {
3483
3482
  uuid: uuid.slice(0, 8),
3484
- hostname: primaryHostname,
3483
+ hostnames: hostnames.join(", "),
3485
3484
  systemName
3486
3485
  });
3487
3486
  let version;
3488
3487
  try {
3489
- version = await firstValueFrom(
3490
- versionDiscovery.discoverVersion(primaryHostname)
3488
+ const winner = await discoverVersionFromAnyHostname(
3489
+ hostnames,
3490
+ versionDiscovery
3491
3491
  );
3492
+ version = winner.version;
3492
3493
  logger.info("API version discovered, instantiating client", {
3493
3494
  uuid: uuid.slice(0, 8),
3495
+ // Which hostname answered is log context only. The client is built with
3496
+ // the full hostname list regardless — the websocket connection races all
3497
+ // of them anyway.
3498
+ hostname: winner.hostname,
3494
3499
  version: version.version,
3495
3500
  websocketPath: version.websocketPath
3496
3501
  });
3497
3502
  } catch (error) {
3498
3503
  const errorMessage = errorMessageOrDefault(error, "Unknown error");
3499
3504
  if (!(error instanceof VersionDiscoveryNetworkError)) {
3500
- logger.error("Version discovery failed", {
3505
+ logger.error("Version discovery failed on every hostname", {
3501
3506
  uuid: uuid.slice(0, 8),
3502
- hostname: primaryHostname,
3507
+ hostnames: hostnames.join(", "),
3503
3508
  error: errorMessage,
3504
3509
  errorType: error instanceof Error ? error.constructor.name : typeof error
3505
3510
  });
@@ -3510,7 +3515,7 @@ async function createTrueNasClient(opts) {
3510
3515
  if (!fallbackVersion) {
3511
3516
  logger.error("Invalid fallback version configuration", {
3512
3517
  uuid: uuid.slice(0, 8),
3513
- hostname: primaryHostname,
3518
+ hostnames: hostnames.join(", "),
3514
3519
  fallbackVersion: fallbackVersionString
3515
3520
  });
3516
3521
  throw error;
@@ -3519,7 +3524,7 @@ async function createTrueNasClient(opts) {
3519
3524
  "Version discovery failed with a network error (possible CORS or network issue), falling back to assumed version",
3520
3525
  {
3521
3526
  uuid: uuid.slice(0, 8),
3522
- hostname: primaryHostname,
3527
+ hostnames: hostnames.join(", "),
3523
3528
  fallbackVersion: fallbackVersionString,
3524
3529
  originalError: errorMessage,
3525
3530
  warning: "A network error has multiple causes (CORS, network down, DNS failure). The connection may still fail during the WebSocket handshake."
@@ -3529,6 +3534,27 @@ async function createTrueNasClient(opts) {
3529
3534
  }
3530
3535
  return instantiateClientForVersion(version, opts, logger);
3531
3536
  }
3537
+ async function discoverVersionFromAnyHostname(hostnames, versionDiscovery) {
3538
+ const attempts = hostnames.map(
3539
+ (hostname) => firstValueFrom(versionDiscovery.discoverVersion(hostname)).then(
3540
+ (version) => ({ hostname, version })
3541
+ )
3542
+ );
3543
+ try {
3544
+ return await Promise.any(attempts);
3545
+ } catch (error) {
3546
+ const failures = error instanceof AggregateError ? error.errors : [error];
3547
+ throw selectRepresentativeFailure(failures);
3548
+ }
3549
+ }
3550
+ function selectRepresentativeFailure(failures) {
3551
+ const isVersionError = (error) => (
3552
+ // cases: valid response, but the given versions won't work for us
3553
+ error instanceof VersionTooOldError || error instanceof VersionTooNewError || error instanceof NoCompatibleVersionsError || error instanceof VersionEndpointNotFoundError
3554
+ );
3555
+ const isNetworkError = (error) => error instanceof VersionDiscoveryNetworkError;
3556
+ return failures.find(isVersionError) ?? failures.find(isNetworkError) ?? failures[0];
3557
+ }
3532
3558
  var CLIENT_BY_VERSION_KEY = {
3533
3559
  "25.10": TrueNasApiClientV2510,
3534
3560
  "26": TrueNasApiClientV26