@truenas/api-client 7.0.0 → 7.0.2

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.
@@ -2904,6 +2904,9 @@ function getHttpError(reason) {
2904
2904
  }
2905
2905
  return "";
2906
2906
  }
2907
+ function getCloseMessage(code, reason) {
2908
+ return isHttpStatusError(reason) ? getHttpError(reason) : getWebSocketError(code);
2909
+ }
2907
2910
  function getWebSocketError(code) {
2908
2911
  switch (code) {
2909
2912
  case 1e3:
@@ -2963,14 +2966,12 @@ var tenSeconds = 10 * 1e3;
2963
2966
  var twentySeconds = 20 * 1e3;
2964
2967
  var TrueNasConnection = class {
2965
2968
  constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:") {
2966
- this.hostnames = hostnames;
2967
2969
  this.systemUuid = systemUuid;
2968
2970
  this.websocketPath = websocketPath;
2969
2971
  this.systemName = systemName;
2970
2972
  this.retryDelay = retryDelay;
2971
2973
  this.maxRetry = maxRetry;
2972
2974
  this.logger = logger;
2973
- this.protocol = protocol;
2974
2975
  // compatibility properties
2975
2976
  this.opened = new rxjs.BehaviorSubject(false);
2976
2977
  this.closed = new rxjs.Subject();
@@ -2986,6 +2987,19 @@ var TrueNasConnection = class {
2986
2987
  * flip it via `setEnabled()` (the app maps `SystemState.Active -> true`).
2987
2988
  */
2988
2989
  this.enabled$ = new rxjs.BehaviorSubject(false);
2990
+ /** Replaced in the constructor, before anything subscribes. See `setEndpoint()`. */
2991
+ this.endpoint$ = new rxjs.BehaviorSubject(resolveEndpoint([], "https:"));
2992
+ /** The endpoint the last attempt raced; a different one starts with `closed`. */
2993
+ this.lastAttempted = null;
2994
+ /** Protected so the testing entry's `FakeConnection` can script closes. */
2995
+ this.closesSubject = new rxjs.Subject();
2996
+ /**
2997
+ * Every socket close this client did not cause itself: failed attempts, lost
2998
+ * connections and refusals, each with its code. Tearing a socket down through
2999
+ * `setEnabled(false)`, `setEndpoint()` or `close()` does not report one, nor
3000
+ * does an attempt that times out unanswered after 10 s, as nothing closed.
3001
+ */
3002
+ this.closes$ = this.closesSubject.asObservable();
2989
3003
  /**
2990
3004
  * observable which emits the current gate value and only emits again when it changes.
2991
3005
  */
@@ -2995,30 +3009,18 @@ var TrueNasConnection = class {
2995
3009
  */
2996
3010
  this.connection$ = this.enabledChange$.pipe(
2997
3011
  rxjs.switchMap((enabled) => {
2998
- if (enabled) {
2999
- return this.connect().pipe(
3000
- rxjs.map((conn) => makeActiveConnection(conn.ws, conn.hostname)),
3001
- // A refusal becomes a value here rather than an error, so it never
3002
- // reaches the `retry` below and nothing reconnects on its own.
3003
- //
3004
- // Handled at this depth on purpose: letting it out completes the
3005
- // pipeline and unsubscribes `enabledChange$`, and `setEnabled` is the
3006
- // documented way an app asks for another attempt once the network
3007
- // changes. Not retrying is a statement about what this client does on
3008
- // its own; it is not a reason to refuse the caller asking again.
3009
- rxjs.catchError(
3010
- (err) => isTerminalClose(err) ? rxjs.of(
3011
- makeConnectionError(
3012
- err.message,
3013
- err.hostname,
3014
- err.closeCode,
3015
- err.closeReason
3016
- )
3017
- ) : rxjs.throwError(() => err)
3018
- )
3019
- );
3012
+ if (!enabled) {
3013
+ return rxjs.of(closedConnection);
3020
3014
  }
3021
- return rxjs.of(closedConnection);
3015
+ return this.endpoint$.pipe(
3016
+ rxjs.distinctUntilChanged(sameEndpoint),
3017
+ rxjs.switchMap((endpoint) => {
3018
+ const changed = this.lastAttempted !== null && !sameEndpoint(this.lastAttempted, endpoint);
3019
+ this.lastAttempted = endpoint;
3020
+ const attempt = this.attempt(endpoint);
3021
+ return changed ? attempt.pipe(rxjs.startWith(closedConnection)) : attempt;
3022
+ })
3023
+ );
3022
3024
  }),
3023
3025
  rxjs.switchMap((connection) => {
3024
3026
  if (connection.state !== "active") {
@@ -3049,16 +3051,27 @@ var TrueNasConnection = class {
3049
3051
  err.closeReason
3050
3052
  )
3051
3053
  ),
3052
- // since this error will immediately get caught by `retry` there's no reason to build a real error.
3053
- rxjs.throwError(() => null)
3054
+ rxjs.throwError(() => err)
3054
3055
  );
3055
3056
  }),
3056
- rxjs.retry(),
3057
+ // A lost live socket reconnects at once. A cycle that never opened waits
3058
+ // `retryDelay` like any other attempt; without it the next cycle's first
3059
+ // attempt followed the last one back to back. Nothing upstream is
3060
+ // subscribed during the wait, so a gate or endpoint change ends it early.
3061
+ rxjs.retry({
3062
+ delay: (err) => err?.wasOpen ? rxjs.of(null) : rxjs.race(
3063
+ rxjs.timer(this.retryDelay),
3064
+ this.enabledChange$.pipe(rxjs.skip(1)),
3065
+ this.endpoint$.pipe(rxjs.skip(1))
3066
+ )
3067
+ }),
3057
3068
  // start with a closed connection.
3058
3069
  rxjs.startWith(closedConnection),
3070
+ // Above `shareReplay`, whose `refCount: false` subscription would otherwise
3071
+ // outlive `close()` and let a pending retry wait open another socket.
3072
+ rxjs.takeUntil(this.closeConnection),
3059
3073
  // prevent multiple subscriptions from re-evaluating the entire pipeline.
3060
- rxjs.shareReplay({ bufferSize: 1, refCount: false }),
3061
- rxjs.takeUntil(this.closeConnection)
3074
+ rxjs.shareReplay({ bufferSize: 1, refCount: false })
3062
3075
  );
3063
3076
  /**
3064
3077
  * observable which is either an instance of the current `TrueNasSocket` used for the connection
@@ -3123,6 +3136,7 @@ var TrueNasConnection = class {
3123
3136
  rxjs.catchError(() => rxjs.EMPTY)
3124
3137
  ))
3125
3138
  );
3139
+ this.endpoint$.next(resolveEndpoint(hostnames, protocol));
3126
3140
  this.ws$.pipe(
3127
3141
  rxjs.switchMap((ws) => ws ? rxjs.interval(twentySeconds).pipe(rxjs.map(() => ws)) : rxjs.EMPTY),
3128
3142
  rxjs.takeUntil(this.closeConnection)
@@ -3136,6 +3150,29 @@ var TrueNasConnection = class {
3136
3150
  this.lastErrorMessage$.subscribe((msg) => this.lastErrorMessage.next(msg));
3137
3151
  this.enabled$.next(initialEnabled);
3138
3152
  }
3153
+ /** The hostnames and protocol the connection currently points at. */
3154
+ get endpoint() {
3155
+ return this.endpoint$.value;
3156
+ }
3157
+ /** The appliance's scheme; changed through `setEndpoint()`. */
3158
+ get protocol() {
3159
+ return this.endpoint$.value.protocol;
3160
+ }
3161
+ /**
3162
+ * Re-points the connection, e.g. after the appliance's GUI address or
3163
+ * protocol changes. Any open socket is closed and, while enabled, the new
3164
+ * hostnames are raced at once; an unchanged endpoint does nothing. The
3165
+ * websocket path stays, so a different API version needs a new client.
3166
+ */
3167
+ setEndpoint(endpoint) {
3168
+ if (endpoint.hostnames.length === 0) {
3169
+ throw new Error("Cannot point the connection at an empty hostnames array");
3170
+ }
3171
+ const next = resolveEndpoint(endpoint.hostnames, endpoint.protocol ?? this.protocol);
3172
+ if (sameEndpoint(next, this.endpoint$.value)) return;
3173
+ this.connectionAttempts.next(0);
3174
+ this.endpoint$.next(next);
3175
+ }
3139
3176
  /**
3140
3177
  * whether the connection has exhausted its retries — the **cumulative** snapshot, read
3141
3178
  * synchronously. (Formerly `hasConnectionError()`; renamed to disambiguate it from the
@@ -3145,7 +3182,7 @@ var TrueNasConnection = class {
3145
3182
  * OR when an error message is currently set. Ported from the source (tncui) behavior.
3146
3183
  */
3147
3184
  hasExhaustedRetries() {
3148
- const attemptsExhausted = this.connectionAttempts.value > this.hostnames.length * this.maxRetry;
3185
+ const attemptsExhausted = this.connectionAttempts.value > this.endpoint.hostnames.length * this.maxRetry;
3149
3186
  const hasErrorMessage = this.lastErrorMessage.value !== null;
3150
3187
  return attemptsExhausted || hasErrorMessage;
3151
3188
  }
@@ -3185,14 +3222,15 @@ var TrueNasConnection = class {
3185
3222
  close() {
3186
3223
  this.closeConnection.next();
3187
3224
  this.closeConnection.complete();
3225
+ this.closesSubject.complete();
3188
3226
  }
3189
3227
  /**
3190
3228
  * helper method which creates an `Observable` which emits an `ActiveConnection`
3191
3229
  * after it establishes a connection to the given hostname. the observable will emit an
3192
3230
  * error if the connection is never established and will not complete until unsubscribed from or closed.
3193
3231
  */
3194
- createSocket(hostname) {
3195
- const url = `${socketScheme(this.protocol)}//${hostname}${this.websocketPath}`;
3232
+ createSocket(hostname, protocol) {
3233
+ const url = `${socketScheme(protocol)}//${hostname}${this.websocketPath}`;
3196
3234
  let hasOpened = false;
3197
3235
  return new rxjs.Observable((subscriber) => {
3198
3236
  const ws = new TrueNasSocket({
@@ -3207,16 +3245,22 @@ var TrueNasConnection = class {
3207
3245
  closeObserver: {
3208
3246
  next: (event) => {
3209
3247
  const reason = event.reason || "";
3210
- let errorMessage;
3211
- if (isHttpStatusError(reason)) {
3212
- errorMessage = getHttpError(reason);
3213
- } else {
3214
- errorMessage = getWebSocketError(event.code);
3248
+ const errorMessage = getCloseMessage(event.code, reason);
3249
+ if (!subscriber.closed) {
3250
+ this.connectionAttempts.next(this.connectionAttempts.value + 1);
3251
+ this.closesSubject.next({
3252
+ code: event.code,
3253
+ reason,
3254
+ message: errorMessage,
3255
+ hostname,
3256
+ wasOpen: hasOpened,
3257
+ refused: event.code === policyViolationCloseCode
3258
+ });
3215
3259
  }
3216
- this.connectionAttempts.next(this.connectionAttempts.value + 1);
3217
- subscriber.error(
3218
- makeConnectionError(errorMessage, hostname, event.code, reason)
3219
- );
3260
+ subscriber.error({
3261
+ ...makeConnectionError(errorMessage, hostname, event.code, reason),
3262
+ wasOpen: hasOpened
3263
+ });
3220
3264
  }
3221
3265
  }
3222
3266
  });
@@ -3230,8 +3274,17 @@ var TrueNasConnection = class {
3230
3274
  };
3231
3275
  }).pipe(
3232
3276
  // retry logic:
3233
- // * if a connection is not established in 10 seconds, consider that an error
3234
- rxjs.timeout({ first: tenSeconds }),
3277
+ // * if a connection is not established in 10 seconds, consider that an error.
3278
+ // Counted here: rxjs never closes a socket still connecting, so no
3279
+ // close event reliably follows, and one that does finds `subscriber`
3280
+ // closed. Nothing on `closes$` either: nothing closed.
3281
+ rxjs.timeout({
3282
+ first: tenSeconds,
3283
+ with: (info) => {
3284
+ this.connectionAttempts.next(this.connectionAttempts.value + 1);
3285
+ return rxjs.throwError(() => new rxjs.TimeoutError(info));
3286
+ }
3287
+ }),
3235
3288
  rxjs.retry({
3236
3289
  // * while still *establishing*: wait `retryDelay` before trying again, giving up
3237
3290
  // after `maxRetry` retries.
@@ -3249,12 +3302,36 @@ var TrueNasConnection = class {
3249
3302
  })
3250
3303
  );
3251
3304
  }
3305
+ /** One attempt at `endpoint`: the race, with a refusal turned into a value. */
3306
+ attempt(endpoint) {
3307
+ return this.connect(endpoint).pipe(
3308
+ rxjs.map((conn) => makeActiveConnection(conn.ws, conn.hostname)),
3309
+ // A refusal becomes a value here rather than an error, so it never
3310
+ // reaches the `retry` in `connection$` and nothing reconnects on its own.
3311
+ //
3312
+ // Handled at this depth on purpose: letting it out completes the
3313
+ // pipeline and unsubscribes `enabledChange$`, and `setEnabled` (or
3314
+ // `setEndpoint`) is how an app asks for another attempt once the network
3315
+ // changes. Not retrying is a statement about what this client does on
3316
+ // its own; it is not a reason to refuse the caller asking again.
3317
+ rxjs.catchError(
3318
+ (err) => isTerminalClose(err) ? rxjs.of(
3319
+ makeConnectionError(
3320
+ err.message,
3321
+ err.hostname,
3322
+ err.closeCode,
3323
+ err.closeReason
3324
+ )
3325
+ ) : rxjs.throwError(() => err)
3326
+ )
3327
+ );
3328
+ }
3252
3329
  /**
3253
3330
  * helper function which actually performs the parallel connection `race`.
3254
3331
  */
3255
- connect() {
3332
+ connect(endpoint) {
3256
3333
  return rxjs.race(
3257
- this.hostnames.map(this.createSocket.bind(this))
3334
+ endpoint.hostnames.map((hostname) => this.createSocket(hostname, endpoint.protocol))
3258
3335
  ).pipe(
3259
3336
  rxjs.tap((conn) => this.logger.debug(`TrueNas socket opened to ${conn.hostname}.`)),
3260
3337
  rxjs.takeUntil(this.closeConnection)
@@ -3275,11 +3352,13 @@ var makeConnectionError = (message, hostname, closeCode, closeReason) => ({
3275
3352
  state: "error"
3276
3353
  });
3277
3354
  var isTerminalClose = (err) => err?.closeCode === policyViolationCloseCode;
3355
+ var resolveEndpoint = (hostnames, protocol) => Object.freeze({ hostnames: Object.freeze([...hostnames]), protocol });
3356
+ var sameEndpoint = (a, b) => a.protocol === b.protocol && a.hostnames.length === b.hostnames.length && a.hostnames.every((hostname, i) => hostname === b.hostnames[i]);
3278
3357
  var closedConnection = { state: "closed" };
3279
3358
 
3280
3359
  // src/client/truenas-api-client.ts
3281
3360
  var TrueNasApiClient = class {
3282
- constructor(uuid, hostnames, version, enabled, systemName, logger = noopLogger, protocol = "https:") {
3361
+ constructor(uuid, hostnames, version, enabled, systemName, logger = noopLogger, protocol = "https:", reconnect = {}) {
3283
3362
  this.uuid = uuid;
3284
3363
  this.hostnames = hostnames;
3285
3364
  this.version = version;
@@ -3287,6 +3366,7 @@ var TrueNasApiClient = class {
3287
3366
  this.systemName = systemName;
3288
3367
  this.logger = logger;
3289
3368
  this.protocol = protocol;
3369
+ this.reconnect = reconnect;
3290
3370
  this.connection = this.createConnection();
3291
3371
  this.authenticator = this.createAuthenticator();
3292
3372
  this.api = this.createApi();
@@ -3325,10 +3405,8 @@ var TrueNasApiClient = class {
3325
3405
  this.uuid,
3326
3406
  websocketPath,
3327
3407
  this.systemName,
3328
- void 0,
3329
- // retryDelay (use default)
3330
- void 0,
3331
- // maxRetry (use default)
3408
+ this.reconnect.retryDelay,
3409
+ this.reconnect.maxRetry,
3332
3410
  this.logger,
3333
3411
  this.protocol
3334
3412
  );
@@ -4184,7 +4262,8 @@ function instantiateClientForVersion(version, opts, logger) {
4184
4262
  enabled,
4185
4263
  systemName,
4186
4264
  logger,
4187
- opts.protocol
4265
+ opts.protocol,
4266
+ { retryDelay: opts.retryDelay, maxRetry: opts.maxRetry }
4188
4267
  );
4189
4268
  }
4190
4269
  function errorMessageOrDefault(error, fallback) {
@@ -4231,9 +4310,11 @@ exports.clientClassFor = clientClassFor;
4231
4310
  exports.consoleLogger = consoleLogger;
4232
4311
  exports.createTrueNasClient = createTrueNasClient;
4233
4312
  exports.getApiErrorMessage = getApiErrorMessage;
4313
+ exports.getCloseMessage = getCloseMessage;
4234
4314
  exports.isJobFinished = isJobFinished;
4235
4315
  exports.noopLogger = noopLogger;
4236
4316
  exports.parseApiVersion = parseApiVersion;
4317
+ exports.policyViolationCloseCode = policyViolationCloseCode;
4237
4318
  exports.v25_10_0_exports = v25_10_0_exports;
4238
4319
  exports.v25_10_1_exports = v25_10_1_exports;
4239
4320
  exports.v25_10_2_exports = v25_10_2_exports;
@@ -4242,5 +4323,5 @@ exports.v25_10_4_exports = v25_10_4_exports;
4242
4323
  exports.v25_10_5_exports = v25_10_5_exports;
4243
4324
  exports.v26_0_0_exports = v26_0_0_exports;
4244
4325
  exports.v27_0_0_exports = v27_0_0_exports;
4245
- //# sourceMappingURL=chunk-QJRF5DK4.cjs.map
4246
- //# sourceMappingURL=chunk-QJRF5DK4.cjs.map
4326
+ //# sourceMappingURL=chunk-SKMB6DV7.cjs.map
4327
+ //# sourceMappingURL=chunk-SKMB6DV7.cjs.map