@truenas/api-client 9.0.2 → 9.0.4

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
@@ -171,6 +171,30 @@ client you get. A named `version` skips discovery and also selects the
171
171
  WebSocket endpoint, so name the version the appliance actually runs. The types
172
172
  follow it when the version is a literal.
173
173
 
174
+ To be written against one version but run against whatever the appliance
175
+ serves, as an appliance's own UI is, keep the `version` and move the socket:
176
+
177
+ ```typescript
178
+ const client = await createTrueNasClient({
179
+ ...options,
180
+ version: 'v28.0.0', // the surface you compile against
181
+ websocketPath: '/api/current', // the appliance's newest version, whichever it is
182
+ });
183
+ ```
184
+
185
+ The client then works against an older or newer release call for call; a
186
+ method or field that release lacks fails when it is called rather than at
187
+ compile time.
188
+
189
+ Each version's models are under its namespace, and its enums are values there
190
+ as well as types, so a request can be built from them:
191
+
192
+ ```typescript
193
+ import { v28_0_0 } from '@truenas/api-client';
194
+
195
+ const type: v28_0_0.KeychainCredentialEntryType = v28_0_0.KeychainCredentialEntryType.SshKeyPair;
196
+ ```
197
+
174
198
  For code that must run against any supported release, use `client.ops`. It
175
199
  offers the same calls on every version and maps them onto whatever that version
176
200
  provides:
@@ -234,6 +258,29 @@ version stays the same, so a different version needs a new client:
234
258
  client.connection.setEndpoint({ hostnames: ['10.0.0.5'], protocol: 'http:' });
235
259
  ```
236
260
 
261
+ An `interceptor` sees the connection's traffic, for a debug panel or for
262
+ mocking calls in a running app. `outgoing` gets every frame before it is
263
+ written, including the client's own `core.set_options` and `core.ping`, and
264
+ returning `false` keeps a frame from `send` off the socket. `injected$` frames
265
+ are read as if the appliance had sent them, one tick later. `incoming` gets
266
+ every frame read:
267
+
268
+ ```typescript
269
+ const mocked$ = new Subject<TrueNasMessage>();
270
+ const client = await createTrueNasClient({
271
+ uuid, hostnames, enabled: true,
272
+ interceptor: {
273
+ outgoing: (frame) => {
274
+ if (frame.method !== 'system.info') return;
275
+ mocked$.next({ jsonrpc: '2.0', id: frame.id, result: fakeInfo });
276
+ return false;
277
+ },
278
+ incoming: (frame) => console.debug('<-', frame),
279
+ injected$: mocked$,
280
+ },
281
+ });
282
+ ```
283
+
237
284
  An appliance served without TLS needs `protocol: 'http:'`, which switches
238
285
  discovery to `http` and the socket to `ws`. It describes the appliance, not the
239
286
  page, and defaults to `https:`.
@@ -1,4 +1,4 @@
1
- import { BehaviorSubject, filter, switchMap, catchError, EMPTY, tap, map, finalize, take, Subject, distinctUntilChanged, of, startWith, concat, throwError, retry, race, timer, skip, takeUntil, shareReplay, interval, Observable, timeout, TimeoutError, toArray, from, defer, firstValueFrom, share, ignoreElements, merge, takeWhile, skipWhile, pairwise } from 'rxjs';
1
+ import { BehaviorSubject, filter, switchMap, catchError, EMPTY, tap, map, finalize, take, Subject, distinctUntilChanged, of, startWith, concat, throwError, retry, race, timer, skip, takeUntil, shareReplay, merge, observeOn, asapScheduler, share, interval, Subscription, Observable, timeout, TimeoutError, toArray, from, defer, firstValueFrom, ignoreElements, takeWhile, skipWhile, pairwise } from 'rxjs';
2
2
  import { webSocket } from 'rxjs/webSocket';
3
3
  import { map as map$1, catchError as catchError$1, shareReplay as shareReplay$1 } from 'rxjs/operators';
4
4
 
@@ -3203,13 +3203,14 @@ function socketScheme(protocol) {
3203
3203
  var tenSeconds = 10 * 1e3;
3204
3204
  var twentySeconds = 20 * 1e3;
3205
3205
  var TrueNasConnection = class {
3206
- constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:") {
3206
+ constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:", interceptor) {
3207
3207
  this.systemUuid = systemUuid;
3208
3208
  this.websocketPath = websocketPath;
3209
3209
  this.systemName = systemName;
3210
3210
  this.retryDelay = retryDelay;
3211
3211
  this.maxRetry = maxRetry;
3212
3212
  this.logger = logger;
3213
+ this.interceptor = interceptor;
3213
3214
  // compatibility properties
3214
3215
  this.opened = new BehaviorSubject(false);
3215
3216
  this.closed = new Subject();
@@ -3267,6 +3268,7 @@ var TrueNasConnection = class {
3267
3268
  const initMessage = createJsonRpcMessage("core.set_options", [
3268
3269
  { legacy_jobs: false }
3269
3270
  ]);
3271
+ this.interceptor?.outgoing?.(initMessage);
3270
3272
  connection.ws.next(initMessage);
3271
3273
  return connection.ws.messages().pipe(
3272
3274
  filter((msg) => msg.id === initMessage.id),
@@ -3365,22 +3367,27 @@ var TrueNasConnection = class {
3365
3367
  map((conn) => conn.state === "error"),
3366
3368
  takeUntil(this.closeConnection)
3367
3369
  );
3368
- /**
3369
- * observable which always emits messages from the current socket.
3370
- */
3371
- this.messages$ = this.ws$.pipe(
3370
+ /** What the appliance sends, on whichever socket is current. */
3371
+ this.socketMessages$ = this.ws$.pipe(
3372
3372
  filter((ws) => ws !== null),
3373
3373
  switchMap((ws) => ws.messages().pipe(
3374
3374
  catchError(() => EMPTY)
3375
3375
  ))
3376
3376
  );
3377
+ this.messages$ = interceptor?.injected$ ? merge(this.socketMessages$, interceptor.injected$.pipe(observeOn(asapScheduler), share())) : this.socketMessages$;
3377
3378
  this.endpoint$.next(resolveEndpoint(hostnames, protocol));
3378
3379
  this.ws$.pipe(
3379
3380
  switchMap((ws) => ws ? interval(twentySeconds).pipe(map(() => ws)) : EMPTY),
3380
3381
  takeUntil(this.closeConnection)
3381
3382
  ).subscribe((ws) => {
3382
- ws.next(createJsonRpcMessage("core.ping"));
3383
+ const ping = createJsonRpcMessage("core.ping");
3384
+ this.interceptor?.outgoing?.(ping);
3385
+ ws.next(ping);
3383
3386
  });
3387
+ if (interceptor?.incoming) {
3388
+ const incoming = interceptor.incoming.bind(interceptor);
3389
+ this.messages$.pipe(takeUntil(this.closeConnection)).subscribe((message) => incoming(message));
3390
+ }
3384
3391
  this.opened$.subscribe((val) => this.opened.next(val));
3385
3392
  this.closed$.subscribe(() => this.closed.next());
3386
3393
  this.hostname$.pipe(filter(Boolean)).subscribe((name) => this.hostname.next(name));
@@ -3449,6 +3456,9 @@ var TrueNasConnection = class {
3449
3456
  * the next `closed$` emission.
3450
3457
  */
3451
3458
  send(message) {
3459
+ if (this.interceptor?.outgoing?.(message) === false) {
3460
+ return Subscription.EMPTY;
3461
+ }
3452
3462
  return this.ws$.pipe(
3453
3463
  filter((ws) => ws !== null),
3454
3464
  take(1)
@@ -3646,7 +3656,8 @@ var TrueNasApiClient = class {
3646
3656
  this.reconnect.retryDelay,
3647
3657
  this.reconnect.maxRetry,
3648
3658
  this.logger,
3649
- this.protocol
3659
+ this.protocol,
3660
+ this.reconnect.interceptor
3650
3661
  );
3651
3662
  }
3652
3663
  /**
@@ -4306,6 +4317,11 @@ async function createTrueNasClient(opts) {
4306
4317
  `Cannot create client for system ${uuid}: hostnames array is empty`
4307
4318
  );
4308
4319
  }
4320
+ if (opts.websocketPath !== void 0 && !opts.websocketPath.startsWith("/")) {
4321
+ throw new Error(
4322
+ `Cannot create client for system ${uuid}: websocketPath must be an absolute path, got '${opts.websocketPath}'.`
4323
+ );
4324
+ }
4309
4325
  logger.info("Creating versioned API client", {
4310
4326
  uuid: uuid.slice(0, 8),
4311
4327
  hostnames: hostnames.join(", "),
@@ -4335,7 +4351,7 @@ async function createTrueNasClient(opts) {
4335
4351
  logger.info("API version supplied by the caller, skipping discovery", {
4336
4352
  uuid: uuid.slice(0, 8),
4337
4353
  version: known.version,
4338
- websocketPath: known.websocketPath
4354
+ websocketPath: opts.websocketPath ?? known.websocketPath
4339
4355
  });
4340
4356
  return instantiateClientForVersion(known, opts, logger);
4341
4357
  }
@@ -4354,7 +4370,7 @@ async function createTrueNasClient(opts) {
4354
4370
  // of them anyway.
4355
4371
  hostname: winner.hostname,
4356
4372
  version: version.version,
4357
- websocketPath: version.websocketPath
4373
+ websocketPath: opts.websocketPath ?? version.websocketPath
4358
4374
  });
4359
4375
  } catch (error) {
4360
4376
  const errorMessage = errorMessageOrDefault(error, "Unknown error");
@@ -4480,6 +4496,7 @@ function clientClassFor(version) {
4480
4496
  function instantiateClientForVersion(version, opts, logger) {
4481
4497
  const { uuid, hostnames, enabled, systemName } = opts;
4482
4498
  const versionKey = clientVersionKey(version);
4499
+ const endpoint = opts.websocketPath === void 0 ? version : { ...version, websocketPath: opts.websocketPath };
4483
4500
  const Client = CLIENT_BY_VERSION_KEY[versionKey];
4484
4501
  if (!Client) {
4485
4502
  logger.error("No client implementation for version", {
@@ -4494,17 +4511,18 @@ function instantiateClientForVersion(version, opts, logger) {
4494
4511
  logger.info(`Instantiating ${Client.name}`, {
4495
4512
  uuid: uuid.slice(0, 8),
4496
4513
  version: version.version,
4497
- versionKey
4514
+ versionKey,
4515
+ websocketPath: endpoint.websocketPath
4498
4516
  });
4499
4517
  return new Client(
4500
4518
  uuid,
4501
4519
  hostnames,
4502
- version,
4520
+ endpoint,
4503
4521
  enabled,
4504
4522
  systemName,
4505
4523
  logger,
4506
4524
  opts.protocol,
4507
- { retryDelay: opts.retryDelay, maxRetry: opts.maxRetry }
4525
+ { retryDelay: opts.retryDelay, maxRetry: opts.maxRetry, interceptor: opts.interceptor }
4508
4526
  );
4509
4527
  }
4510
4528
  function errorMessageOrDefault(error, fallback) {
@@ -4524,5 +4542,5 @@ var UserRole = /* @__PURE__ */ ((UserRole2) => {
4524
4542
  })(UserRole || {});
4525
4543
 
4526
4544
  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 };
4527
- //# sourceMappingURL=chunk-PJVDHM2S.js.map
4528
- //# sourceMappingURL=chunk-PJVDHM2S.js.map
4545
+ //# sourceMappingURL=chunk-PEOXWWBS.js.map
4546
+ //# sourceMappingURL=chunk-PEOXWWBS.js.map