@truenas/api-client 9.0.5 → 10.0.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.
@@ -1731,6 +1731,7 @@ __export(v27_0_0_exports, {
1731
1731
  IscsiTargetCreateModeInput: () => IscsiTargetCreateModeInput,
1732
1732
  KeychainCredentialEntryType: () => KeychainCredentialEntryType,
1733
1733
  KeychainCredentialEntryTypeInput: () => KeychainCredentialEntryTypeInput,
1734
+ Layout: () => Layout,
1734
1735
  Level: () => Level,
1735
1736
  NFS4ACEType: () => NFS4ACEType,
1736
1737
  NFS4ACETypeInput: () => NFS4ACETypeInput,
@@ -1845,6 +1846,10 @@ var Interval = {
1845
1846
  "30": 30,
1846
1847
  "60": 60
1847
1848
  };
1849
+ var Layout = {
1850
+ Bucket: "BUCKET",
1851
+ Dataset: "DATASET"
1852
+ };
1848
1853
  var Name2 = {
1849
1854
  Cpu: "cpu",
1850
1855
  Cputemp: "cputemp",
@@ -1988,6 +1993,7 @@ __export(v28_0_0_exports, {
1988
1993
  IscsiTargetCreateModeInput: () => IscsiTargetCreateModeInput,
1989
1994
  KeychainCredentialEntryType: () => KeychainCredentialEntryType,
1990
1995
  KeychainCredentialEntryTypeInput: () => KeychainCredentialEntryTypeInput,
1996
+ Layout: () => Layout,
1991
1997
  Level: () => Level,
1992
1998
  NFS4ACEType: () => NFS4ACEType,
1993
1999
  NFS4ACETypeInput: () => NFS4ACETypeInput,
@@ -2063,6 +2069,8 @@ __export(v28_0_0_exports, {
2063
2069
  Versioning: () => Versioning,
2064
2070
  Volblocksize: () => Volblocksize,
2065
2071
  When: () => When,
2072
+ ZFSResourceUserspaceEntryType: () => ZFSResourceUserspaceEntryType,
2073
+ ZFSResourceUserspaceEntryTypeInput: () => ZFSResourceUserspaceEntryTypeInput,
2066
2074
  ZfsTierRewriteJobEntryStatus: () => ZfsTierRewriteJobEntryStatus,
2067
2075
  ZfsTierRewriteJobEntryStatusInput: () => ZfsTierRewriteJobEntryStatusInput,
2068
2076
  ZpoolScrubRunAction: () => ZpoolScrubRunAction
@@ -2148,6 +2156,14 @@ var VMWareMatchDatastoresWithDatasetsResultFilesystemTypeInput = {
2148
2156
  Filesystem: "FILESYSTEM",
2149
2157
  Volume: "VOLUME"
2150
2158
  };
2159
+ var ZFSResourceUserspaceEntryType = {
2160
+ User: "user",
2161
+ Group: "group"
2162
+ };
2163
+ var ZFSResourceUserspaceEntryTypeInput = {
2164
+ User: "user",
2165
+ Group: "group"
2166
+ };
2151
2167
 
2152
2168
  // src/generated/index.ts
2153
2169
  var SUPPORTED_API_VERSIONS = [
@@ -3175,6 +3191,157 @@ function getWebSocketError(code) {
3175
3191
  return "Network connection failed - Check IP address and connectivity";
3176
3192
  }
3177
3193
  }
3194
+ var CallLimiter = class {
3195
+ constructor(limit, write) {
3196
+ this.limit = limit;
3197
+ this.write = write;
3198
+ /**
3199
+ * Each request awaiting an answer, with the handle `write` gave for it. That
3200
+ * handle closes once the frame is on a socket: until then the connection is
3201
+ * still holding it for one, and giving up on it takes it back.
3202
+ */
3203
+ this.inFlight = /* @__PURE__ */ new Map();
3204
+ this.waiting = [];
3205
+ /**
3206
+ * Calls that were waiting when a socket was lost, kept apart from `waiting`
3207
+ * until `release()`. A call sent after the loss, the sign-in among them, then
3208
+ * never waits behind one: nothing would write it, and `release()` waits on it.
3209
+ */
3210
+ this.heldCalls = [];
3211
+ }
3212
+ /** Requests written or being written, and not answered yet. */
3213
+ get inFlightCount() {
3214
+ return this.inFlight.size;
3215
+ }
3216
+ /** Requests waiting for a place, held ones included. */
3217
+ get waitingCount() {
3218
+ return this.waiting.length + this.heldCalls.length;
3219
+ }
3220
+ /** Whether calls are being kept from a lost socket until `release()`. */
3221
+ get held() {
3222
+ return this.heldCalls.length > 0;
3223
+ }
3224
+ /**
3225
+ * Sends `message` now if there is room, or queues it. Frames that are not
3226
+ * requests pass straight through. Unsubscribing the returned handle takes
3227
+ * the call back if it has not been written yet.
3228
+ *
3229
+ * New calls are not held back by a lost socket, only by the limit: the frames
3230
+ * that sign a new socket in come through here, and the held calls are
3231
+ * waiting on exactly those.
3232
+ */
3233
+ send(message) {
3234
+ if (!isRequest(message)) {
3235
+ return this.write(message);
3236
+ }
3237
+ const call = { id: message.id, message, handle: new Subscription() };
3238
+ if (this.inFlight.size < this.limit) {
3239
+ this.start(call);
3240
+ return call.handle;
3241
+ }
3242
+ this.waiting.push(call);
3243
+ call.handle.add(() => {
3244
+ remove(this.waiting, call);
3245
+ remove(this.heldCalls, call);
3246
+ });
3247
+ return call.handle;
3248
+ }
3249
+ /**
3250
+ * Counts a request the connection wrote to a socket itself, such as its
3251
+ * keepalive `core.ping`, or returns `false` without counting it when there is
3252
+ * no room. A request written that way is not the limiter's to queue: the
3253
+ * caller decides what not sending it means.
3254
+ */
3255
+ tryTrack(id) {
3256
+ if (this.inFlight.size >= this.limit) {
3257
+ return false;
3258
+ }
3259
+ this.inFlight.set(id, Subscription.EMPTY);
3260
+ return true;
3261
+ }
3262
+ /** Frees the place of the request this frame answers, if it is one. */
3263
+ received(message) {
3264
+ if (message.id === void 0 || message.id === null || message.method) {
3265
+ return;
3266
+ }
3267
+ this.finish(String(message.id));
3268
+ }
3269
+ /**
3270
+ * What was written on the lost socket is lost with it, answers included, so
3271
+ * those places are freed. A frame the connection has not written yet is still
3272
+ * its to send on the next socket, so it stays counted. Calls still waiting are
3273
+ * held until `release()`: the new socket starts signed out, and a call sent on
3274
+ * it before the session is back is refused.
3275
+ */
3276
+ socketLost() {
3277
+ this.inFlight.forEach((sending, id) => {
3278
+ if (sending.closed) {
3279
+ this.inFlight.delete(id);
3280
+ }
3281
+ });
3282
+ this.heldCalls.push(...this.waiting.splice(0));
3283
+ }
3284
+ /**
3285
+ * Lets held calls go out again, once the session they were made in is back.
3286
+ * They were sent before anything still waiting, so they go first.
3287
+ */
3288
+ release() {
3289
+ this.waiting.unshift(...this.heldCalls.splice(0));
3290
+ this.startWaiting();
3291
+ }
3292
+ /**
3293
+ * Takes back every call held from a lost socket, unsent, and returns their
3294
+ * ids so the caller can answer them. Does nothing while nothing is held.
3295
+ */
3296
+ takeHeld() {
3297
+ return this.heldCalls.splice(0).map(({ id }) => id);
3298
+ }
3299
+ // The three below call each other in a ring.
3300
+ start({ id, message, handle }) {
3301
+ this.inFlight.set(id, Subscription.EMPTY);
3302
+ let sending;
3303
+ try {
3304
+ sending = this.write(message);
3305
+ } catch (error) {
3306
+ this.finish(id);
3307
+ throw error;
3308
+ }
3309
+ if (!this.inFlight.has(id)) {
3310
+ return;
3311
+ }
3312
+ this.inFlight.set(id, sending);
3313
+ handle.add(() => {
3314
+ const isWritten = sending.closed;
3315
+ sending.unsubscribe();
3316
+ if (!isWritten && this.inFlight.get(id) === sending) {
3317
+ this.finish(id);
3318
+ }
3319
+ });
3320
+ }
3321
+ startWaiting() {
3322
+ while (this.inFlight.size < this.limit) {
3323
+ const next = this.waiting.shift();
3324
+ if (!next) {
3325
+ return;
3326
+ }
3327
+ this.start(next);
3328
+ }
3329
+ }
3330
+ finish(id) {
3331
+ if (this.inFlight.delete(id)) {
3332
+ this.startWaiting();
3333
+ }
3334
+ }
3335
+ };
3336
+ function remove(calls, call) {
3337
+ const index = calls.indexOf(call);
3338
+ if (index >= 0) {
3339
+ calls.splice(index, 1);
3340
+ }
3341
+ }
3342
+ function isRequest(message) {
3343
+ return message.id !== void 0 && message.id !== null && Boolean(message.method);
3344
+ }
3178
3345
  var TrueNasSocket = class {
3179
3346
  constructor(config) {
3180
3347
  this.config = config;
@@ -3203,7 +3370,7 @@ function socketScheme(protocol) {
3203
3370
  var tenSeconds = 10 * 1e3;
3204
3371
  var twentySeconds = 20 * 1e3;
3205
3372
  var TrueNasConnection = class {
3206
- constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:", interceptor) {
3373
+ constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:", interceptor, maxConcurrentCalls) {
3207
3374
  this.systemUuid = systemUuid;
3208
3375
  this.websocketPath = websocketPath;
3209
3376
  this.systemName = systemName;
@@ -3211,6 +3378,7 @@ var TrueNasConnection = class {
3211
3378
  this.maxRetry = maxRetry;
3212
3379
  this.logger = logger;
3213
3380
  this.interceptor = interceptor;
3381
+ this.maxConcurrentCalls = maxConcurrentCalls;
3214
3382
  // compatibility properties
3215
3383
  this.opened = new BehaviorSubject(false);
3216
3384
  this.closed = new Subject();
@@ -3374,16 +3542,39 @@ var TrueNasConnection = class {
3374
3542
  catchError(() => EMPTY)
3375
3543
  ))
3376
3544
  );
3377
- this.messages$ = interceptor?.injected$ ? merge(this.socketMessages$, interceptor.injected$.pipe(observeOn(asapScheduler), share())) : this.socketMessages$;
3545
+ /** Answers given here, to calls ended without being sent. See `refuseHeldCalls()`. */
3546
+ this.refusals$ = new Subject();
3547
+ if (maxConcurrentCalls !== void 0 && !(Number.isInteger(maxConcurrentCalls) && maxConcurrentCalls > 0)) {
3548
+ throw new Error(`maxConcurrentCalls must be a positive integer, got ${String(maxConcurrentCalls)}`);
3549
+ }
3550
+ this.limiter = maxConcurrentCalls === void 0 ? null : new CallLimiter(maxConcurrentCalls, (message) => this.writeToNextSocket(message));
3551
+ this.messages$ = interceptor?.injected$ ? merge(this.socketMessages$, this.refusals$, interceptor.injected$.pipe(observeOn(asapScheduler), share())) : merge(this.socketMessages$, this.refusals$);
3378
3552
  this.endpoint$.next(resolveEndpoint(hostnames, protocol));
3379
3553
  this.ws$.pipe(
3380
3554
  switchMap((ws) => ws ? interval(twentySeconds).pipe(map(() => ws)) : EMPTY),
3381
3555
  takeUntil(this.closeConnection)
3382
3556
  ).subscribe((ws) => {
3383
3557
  const ping = createJsonRpcMessage("core.ping");
3558
+ if (this.limiter && !this.limiter.tryTrack(ping.id ?? "")) {
3559
+ this.logger.debug("Skipping core.ping: every concurrent call place is taken");
3560
+ return;
3561
+ }
3384
3562
  this.interceptor?.outgoing?.(ping);
3385
3563
  ws.next(ping);
3386
3564
  });
3565
+ if (this.limiter) {
3566
+ const limiter = this.limiter;
3567
+ this.socketMessages$.pipe(takeUntil(this.closeConnection)).subscribe((message) => {
3568
+ limiter.received(message);
3569
+ });
3570
+ let wasOpen = false;
3571
+ this.opened$.pipe(takeUntil(this.closeConnection)).subscribe((isOpen) => {
3572
+ if (wasOpen && !isOpen) {
3573
+ limiter.socketLost();
3574
+ }
3575
+ wasOpen = isOpen;
3576
+ });
3577
+ }
3387
3578
  if (interceptor?.incoming) {
3388
3579
  const incoming = interceptor.incoming.bind(interceptor);
3389
3580
  this.messages$.pipe(takeUntil(this.closeConnection)).subscribe((message) => incoming(message));
@@ -3454,11 +3645,46 @@ var TrueNasConnection = class {
3454
3645
  * sends a message over the current websocket OR queues it to send once
3455
3646
  * the next websocket is opened. messages sent via `send` will not be lost until after
3456
3647
  * the next `closed$` emission.
3648
+ *
3649
+ * With `maxConcurrentCalls` set, a request beyond the limit waits for an
3650
+ * answer to make room. Unsubscribing the returned handle takes back a frame
3651
+ * not written yet, waiting or queued.
3457
3652
  */
3458
3653
  send(message) {
3459
3654
  if (this.interceptor?.outgoing?.(message) === false) {
3460
3655
  return Subscription.EMPTY;
3461
3656
  }
3657
+ return this.limiter ? this.limiter.send(message) : this.writeToNextSocket(message);
3658
+ }
3659
+ /**
3660
+ * Lets calls held from a lost socket go out again. The client calls this
3661
+ * whenever the session is authenticated, so a consumer does not need to;
3662
+ * a no-op without `maxConcurrentCalls`, or with nothing held.
3663
+ *
3664
+ * Calls waiting for a place when a socket is lost are held, not sent on the
3665
+ * next one: a new socket starts signed out, so middleware would refuse them
3666
+ * with `ENOTAUTHENTICATED`.
3667
+ */
3668
+ releaseHeldCalls() {
3669
+ this.limiter?.release();
3670
+ }
3671
+ /**
3672
+ * Ends every call held from a lost socket without sending it, answering each
3673
+ * on `messages()` with the refusal middleware would have given it on a socket
3674
+ * that is not signed in: `ENOTAUTHENTICATED`. Returns how many it ended.
3675
+ *
3676
+ * For an app whose next sign-in may be a different user than the one who made
3677
+ * those calls, e.g. a password prompt after the session was lost. Call it
3678
+ * before that sign-in; otherwise the calls go out once it succeeds. A no-op
3679
+ * while nothing is held, so a healthy session's waiting calls are not touched.
3680
+ */
3681
+ refuseHeldCalls() {
3682
+ const ids = this.limiter?.takeHeld() ?? [];
3683
+ ids.forEach((id) => this.refusals$.next(notAuthenticatedRefusal(id)));
3684
+ return ids.length;
3685
+ }
3686
+ /** Writes to the current socket, or to the next one once it opens. */
3687
+ writeToNextSocket(message) {
3462
3688
  return this.ws$.pipe(
3463
3689
  filter((ws) => ws !== null),
3464
3690
  take(1)
@@ -3602,6 +3828,22 @@ var makeConnectionError = (message, hostname, closeCode, closeReason) => ({
3602
3828
  var isTerminalClose = (err) => err?.closeCode === policyViolationCloseCode;
3603
3829
  var resolveEndpoint = (hostnames, protocol) => Object.freeze({ hostnames: Object.freeze([...hostnames]), protocol });
3604
3830
  var sameEndpoint = (a, b) => a.protocol === b.protocol && a.hostnames.length === b.hostnames.length && a.hostnames.every((hostname, i) => hostname === b.hostnames[i]);
3831
+ var notAuthenticatedRefusal = (id) => ({
3832
+ jsonrpc: "2.0",
3833
+ id,
3834
+ error: {
3835
+ code: -32001,
3836
+ message: "Method call error",
3837
+ data: {
3838
+ // `ErrnoMixin.ENOTAUTHENTICATED` in truenas_api_client.
3839
+ error: 207,
3840
+ errname: "ENOTAUTHENTICATED",
3841
+ reason: "The session this call was made in ended before it could be sent.",
3842
+ extra: null,
3843
+ trace: null
3844
+ }
3845
+ }
3846
+ });
3605
3847
  var closedConnection = { state: "closed" };
3606
3848
 
3607
3849
  // src/client/truenas-api-client.ts
@@ -3619,6 +3861,11 @@ var TrueNasApiClient = class {
3619
3861
  this.authenticator = this.createAuthenticator();
3620
3862
  this.api = this.createApi();
3621
3863
  this.ops = this.createOperations();
3864
+ this.authenticator.authenticated$.pipe(
3865
+ distinctUntilChanged(),
3866
+ filter(Boolean),
3867
+ takeUntil(this.connection.closeConnection)
3868
+ ).subscribe(() => this.connection.releaseHeldCalls());
3622
3869
  }
3623
3870
  /**
3624
3871
  * Get current connection status.
@@ -3657,7 +3904,8 @@ var TrueNasApiClient = class {
3657
3904
  this.reconnect.maxRetry,
3658
3905
  this.logger,
3659
3906
  this.protocol,
3660
- this.reconnect.interceptor
3907
+ this.reconnect.interceptor,
3908
+ this.reconnect.maxConcurrentCalls
3661
3909
  );
3662
3910
  }
3663
3911
  /**
@@ -3973,7 +4221,12 @@ function toContainer3(container) {
3973
4221
  return {
3974
4222
  id: container.id.toString(),
3975
4223
  name: container.name,
3976
- status: toAppState(container.status.state),
4224
+ // `status` is nullable from v28. Middleware's own word on when: "`null` if
4225
+ // `query-options.extra.retrieve_status` is `false`" — so a null is status
4226
+ // that was not fetched, not a container in an unreportable state. That is
4227
+ // what `Unknown` is for ("no usable state"), and it keeps `Container.status`
4228
+ // non-optional for every caller.
4229
+ status: container.status ? toAppState(container.status.state) : "UNKNOWN" /* Unknown */,
3977
4230
  // Optional in the generated entry, required by `Container`.
3978
4231
  autostart: container.autostart ?? false,
3979
4232
  description
@@ -4522,7 +4775,12 @@ function instantiateClientForVersion(version, opts, logger) {
4522
4775
  systemName,
4523
4776
  logger,
4524
4777
  opts.protocol,
4525
- { retryDelay: opts.retryDelay, maxRetry: opts.maxRetry, interceptor: opts.interceptor }
4778
+ {
4779
+ retryDelay: opts.retryDelay,
4780
+ maxRetry: opts.maxRetry,
4781
+ interceptor: opts.interceptor,
4782
+ maxConcurrentCalls: opts.maxConcurrentCalls
4783
+ }
4526
4784
  );
4527
4785
  }
4528
4786
  function errorMessageOrDefault(error, fallback) {
@@ -4542,5 +4800,5 @@ var UserRole = /* @__PURE__ */ ((UserRole2) => {
4542
4800
  })(UserRole || {});
4543
4801
 
4544
4802
  export { AppState, AuthError, AuthErrorCode, AuthResponseType, InvalidVersionResponseError, JobState, NoCompatibleVersionsError, SUPPORTED_API_VERSIONS, TrueNasApiClient, TrueNasApiClientV2510, TrueNasApiClientV27, TrueNasApiClientV28, TrueNasApiError, TrueNasAuthMechanism, TrueNasAuthenticator, TrueNasConnection, UserRole, VersionCompatibility, VersionDiscovery, VersionDiscoveryError, VersionDiscoveryNetworkError, VersionDiscoveryTimeoutError, VersionEndpointNotFoundError, VersionTooNewError, VersionTooOldError, clientClassFor, consoleLogger, createTrueNasClient, getApiErrorMessage, getCloseMessage, isJobFinished, noopLogger, parseApiVersion, policyViolationCloseCode, subscriptionName, v25_10_0_exports, v25_10_1_exports, v25_10_2_exports, v25_10_3_exports, v25_10_4_exports, v25_10_5_exports, v27_0_0_exports, v28_0_0_exports };
4545
- //# sourceMappingURL=chunk-A2RDZXMF.js.map
4546
- //# sourceMappingURL=chunk-A2RDZXMF.js.map
4803
+ //# sourceMappingURL=chunk-Z5U5ZRKJ.js.map
4804
+ //# sourceMappingURL=chunk-Z5U5ZRKJ.js.map