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