@truenas/api-client 8.0.0 → 9.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.
@@ -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, merge, takeWhile } 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, interval, Observable, timeout, TimeoutError, toArray, from, defer, firstValueFrom, share, ignoreElements, merge, 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
 
@@ -28,6 +28,34 @@ function getApiErrorMessage(error, fallback = "API call failed") {
28
28
  return fallback;
29
29
  }
30
30
 
31
+ // src/errors/api.errors.ts
32
+ var TrueNasApiError = class _TrueNasApiError extends Error {
33
+ constructor(frame, method) {
34
+ super(getApiErrorMessage(frame, "API call failed"));
35
+ this.frame = frame;
36
+ this.method = method;
37
+ this.name = "TrueNasApiError";
38
+ Object.setPrototypeOf(this, _TrueNasApiError.prototype);
39
+ }
40
+ /** The JSON-RPC code: `-32001` for a refused call, others for a malformed one. */
41
+ get code() {
42
+ return this.frame.code;
43
+ }
44
+ /**
45
+ * The TrueNAS payload — `errname`, `reason`, `extra`, `trace`.
46
+ *
47
+ * `undefined` for the codes middleware sends without one, such as
48
+ * `-32601 "Method does not exist"`.
49
+ */
50
+ get data() {
51
+ return this.frame.data;
52
+ }
53
+ /** `data.errname`, e.g. `EINVAL`, or `undefined` when there is no payload. */
54
+ get errname() {
55
+ return this.frame.data?.errname;
56
+ }
57
+ };
58
+
31
59
  // src/types/job.type.ts
32
60
  var JobState = /* @__PURE__ */ ((JobState2) => {
33
61
  JobState2["Pending"] = "PENDING";
@@ -1646,9 +1674,9 @@ __export(v25_10_5_exports, {
1646
1674
  ZfsTierRewriteJobEntryStatusInput: () => ZfsTierRewriteJobEntryStatusInput
1647
1675
  });
1648
1676
 
1649
- // src/generated/v26_0_0/index.ts
1650
- var v26_0_0_exports = {};
1651
- __export(v26_0_0_exports, {
1677
+ // src/generated/v27_0_0/index.ts
1678
+ var v27_0_0_exports = {};
1679
+ __export(v27_0_0_exports, {
1652
1680
  ACLTemplateEntryAcltypeInput: () => ACLTemplateEntryAcltypeInput,
1653
1681
  Access: () => Access,
1654
1682
  Aclmode: () => Aclmode,
@@ -1781,7 +1809,7 @@ __export(v26_0_0_exports, {
1781
1809
  ZpoolScrubRunAction: () => ZpoolScrubRunAction
1782
1810
  });
1783
1811
 
1784
- // src/generated/v26_0_0/api-types.ts
1812
+ // src/generated/v27_0_0/api-types.ts
1785
1813
  var Access = {
1786
1814
  Readonly: "READONLY",
1787
1815
  Writeonly: "WRITEONLY",
@@ -1903,9 +1931,9 @@ var ZpoolScrubRunAction = {
1903
1931
  Cancel: "CANCEL"
1904
1932
  };
1905
1933
 
1906
- // src/generated/v27_0_0/index.ts
1907
- var v27_0_0_exports = {};
1908
- __export(v27_0_0_exports, {
1934
+ // src/generated/v28_0_0/index.ts
1935
+ var v28_0_0_exports = {};
1936
+ __export(v28_0_0_exports, {
1909
1937
  ACLTemplateEntryAcltypeInput: () => ACLTemplateEntryAcltypeInput,
1910
1938
  Access: () => Access,
1911
1939
  Aclmode: () => Aclmode,
@@ -1928,7 +1956,7 @@ __export(v27_0_0_exports, {
1928
1956
  CloudTaskAttributesEncryption: () => CloudTaskAttributesEncryption,
1929
1957
  CloudTaskAttributesEncryptionInput: () => CloudTaskAttributesEncryptionInput,
1930
1958
  Code: () => Code,
1931
- Compression: () => Compression,
1959
+ Compression: () => Compression2,
1932
1960
  ContainerNICDeviceType: () => ContainerNICDeviceType,
1933
1961
  ContainerNICDeviceTypeInput: () => ContainerNICDeviceTypeInput,
1934
1962
  ContainerStatusState: () => ContainerStatusState,
@@ -2006,6 +2034,7 @@ __export(v27_0_0_exports, {
2006
2034
  Snapdir: () => Snapdir,
2007
2035
  SourceInput: () => SourceInput,
2008
2036
  SourceValueType: () => SourceValueType,
2037
+ SourceValueTypeInput: () => SourceValueTypeInput,
2009
2038
  SupportNewTicketCommunityTypeInput: () => SupportNewTicketCommunityTypeInput,
2010
2039
  Sync: () => Sync,
2011
2040
  SyslogServerTransport: () => SyslogServerTransport,
@@ -2039,7 +2068,67 @@ __export(v27_0_0_exports, {
2039
2068
  ZpoolScrubRunAction: () => ZpoolScrubRunAction
2040
2069
  });
2041
2070
 
2042
- // src/generated/v27_0_0/api-types.ts
2071
+ // src/generated/v28_0_0/api-types.ts
2072
+ var Compression2 = {
2073
+ On: "ON",
2074
+ Off: "OFF",
2075
+ Lz4: "LZ4",
2076
+ Gzip: "GZIP",
2077
+ Gzip1: "GZIP-1",
2078
+ Gzip2: "GZIP-2",
2079
+ Gzip3: "GZIP-3",
2080
+ Gzip4: "GZIP-4",
2081
+ Gzip5: "GZIP-5",
2082
+ Gzip6: "GZIP-6",
2083
+ Gzip7: "GZIP-7",
2084
+ Gzip8: "GZIP-8",
2085
+ Gzip9: "GZIP-9",
2086
+ Zstd: "ZSTD",
2087
+ ZstdFast: "ZSTD-FAST",
2088
+ Zle: "ZLE",
2089
+ Lzjb: "LZJB",
2090
+ Zstd1: "ZSTD-1",
2091
+ Zstd2: "ZSTD-2",
2092
+ Zstd3: "ZSTD-3",
2093
+ Zstd4: "ZSTD-4",
2094
+ Zstd5: "ZSTD-5",
2095
+ Zstd6: "ZSTD-6",
2096
+ Zstd7: "ZSTD-7",
2097
+ Zstd8: "ZSTD-8",
2098
+ Zstd9: "ZSTD-9",
2099
+ Zstd10: "ZSTD-10",
2100
+ Zstd11: "ZSTD-11",
2101
+ Zstd12: "ZSTD-12",
2102
+ Zstd13: "ZSTD-13",
2103
+ Zstd14: "ZSTD-14",
2104
+ Zstd15: "ZSTD-15",
2105
+ Zstd16: "ZSTD-16",
2106
+ Zstd17: "ZSTD-17",
2107
+ Zstd18: "ZSTD-18",
2108
+ Zstd19: "ZSTD-19",
2109
+ ZstdFast1: "ZSTD-FAST-1",
2110
+ ZstdFast2: "ZSTD-FAST-2",
2111
+ ZstdFast3: "ZSTD-FAST-3",
2112
+ ZstdFast4: "ZSTD-FAST-4",
2113
+ ZstdFast5: "ZSTD-FAST-5",
2114
+ ZstdFast6: "ZSTD-FAST-6",
2115
+ ZstdFast7: "ZSTD-FAST-7",
2116
+ ZstdFast8: "ZSTD-FAST-8",
2117
+ ZstdFast9: "ZSTD-FAST-9",
2118
+ ZstdFast10: "ZSTD-FAST-10",
2119
+ ZstdFast20: "ZSTD-FAST-20",
2120
+ ZstdFast30: "ZSTD-FAST-30",
2121
+ ZstdFast40: "ZSTD-FAST-40",
2122
+ ZstdFast50: "ZSTD-FAST-50",
2123
+ ZstdFast60: "ZSTD-FAST-60",
2124
+ ZstdFast70: "ZSTD-FAST-70",
2125
+ ZstdFast80: "ZSTD-FAST-80",
2126
+ ZstdFast90: "ZSTD-FAST-90",
2127
+ ZstdFast100: "ZSTD-FAST-100",
2128
+ ZstdFast500: "ZSTD-FAST-500",
2129
+ ZstdFast1000: "ZSTD-FAST-1000",
2130
+ Inherit: "INHERIT"
2131
+ };
2043
2132
  var Service2 = {
2044
2133
  Middleware: "MIDDLEWARE",
2045
2134
  S3: "S3",
@@ -2047,6 +2136,14 @@ var Service2 = {
2047
2136
  Sudo: "SUDO",
2048
2137
  System: "SYSTEM"
2049
2138
  };
2139
+ var SourceValueTypeInput = {
2140
+ None: "NONE",
2141
+ Default: "DEFAULT",
2142
+ Temporary: "TEMPORARY",
2143
+ Local: "LOCAL",
2144
+ Inherited: "INHERITED",
2145
+ Received: "RECEIVED"
2146
+ };
2050
2147
  var VMWareMatchDatastoresWithDatasetsResultFilesystemTypeInput = {
2051
2148
  Filesystem: "FILESYSTEM",
2052
2149
  Volume: "VOLUME"
@@ -2060,8 +2157,8 @@ var SUPPORTED_API_VERSIONS = [
2060
2157
  "v25.10.3",
2061
2158
  "v25.10.4",
2062
2159
  "v25.10.5",
2063
- "v26.0.0",
2064
- "v27.0.0"
2160
+ "v27.0.0",
2161
+ "v28.0.0"
2065
2162
  ];
2066
2163
 
2067
2164
  // src/types/api-version.type.ts
@@ -2069,6 +2166,7 @@ var VersionCompatibility = /* @__PURE__ */ ((VersionCompatibility2) => {
2069
2166
  VersionCompatibility2["Compatible"] = "compatible";
2070
2167
  VersionCompatibility2["TooOld"] = "too-old";
2071
2168
  VersionCompatibility2["TooNew"] = "too-new";
2169
+ VersionCompatibility2["Unsupported"] = "unsupported";
2072
2170
  VersionCompatibility2["Invalid"] = "invalid";
2073
2171
  return VersionCompatibility2;
2074
2172
  })(VersionCompatibility || {});
@@ -2152,6 +2250,19 @@ function createJsonRpcMessage(method, params) {
2152
2250
  };
2153
2251
  }
2154
2252
 
2253
+ // src/utils/subscription.utils.ts
2254
+ function subscriptionName(event, params) {
2255
+ return params === void 0 ? event : `${event}:${JSON.stringify(params, sortKeys)}`;
2256
+ }
2257
+ function sortKeys(_key, value) {
2258
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2259
+ return value;
2260
+ }
2261
+ return Object.fromEntries(
2262
+ Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
2263
+ );
2264
+ }
2265
+
2155
2266
  // src/api/truenas-api.ts
2156
2267
  var EVENT_KINDS = ["added", "changed", "removed"];
2157
2268
  var TrueNasApi = class {
@@ -2161,8 +2272,13 @@ var TrueNasApi = class {
2161
2272
  /**
2162
2273
  * Stream of job events from websocket.
2163
2274
  * JSON-RPC 2.0 events have structure: { method: 'collection_update', params: { collection, fields, ... } }
2275
+ *
2276
+ * Reads `messages()` when first subscribed, not when this is built, as every
2277
+ * other verb does. A consumer that wraps the connection's `messages()` after
2278
+ * the client is created — a debug panel mixing in mocked answers — is
2279
+ * otherwise heard by `call` and `events` but not by job tracking.
2164
2280
  */
2165
- this.jobEvents = this.connection.messages().pipe(
2281
+ this.jobEvents = defer(() => this.connection.messages()).pipe(
2166
2282
  filter(
2167
2283
  (res) => res.method === "collection_update" && res.params?.collection === "core.get_jobs"
2168
2284
  ),
@@ -2171,11 +2287,20 @@ var TrueNasApi = class {
2171
2287
  share()
2172
2288
  );
2173
2289
  /**
2174
- * One shared stream per subscribed event name — see {@link events}.
2175
- * Keyed by name rather than by caller, because the subscription it stands
2176
- * for lives on the server and is per socket, not per caller.
2290
+ * One shared stream per subscription — see {@link events}. Keyed by the
2291
+ * name *with* its arguments: a tail of one file is not a tail of another.
2177
2292
  */
2178
2293
  this.eventStreams = /* @__PURE__ */ new Map();
2294
+ /**
2295
+ * Releases awaiting their `core.unsubscribe` reply, per name. Middleware
2296
+ * sends a source's release notice (`terminate`) before that reply, and a
2297
+ * notice names only the collection; so an error-free one arriving inside
2298
+ * that window belongs to the release, not to a later subscription. One per
2299
+ * window, which closes on the reply whether or not a notice came.
2300
+ */
2301
+ this.releasing = /* @__PURE__ */ new Map();
2302
+ /** The live subscription per name, told when the server ends it. */
2303
+ this.serverEnds = /* @__PURE__ */ new Map();
2179
2304
  this.initializeJobEventsSubscription();
2180
2305
  }
2181
2306
  /**
@@ -2213,11 +2338,7 @@ var TrueNasApi = class {
2213
2338
  withId(messageId),
2214
2339
  map((msg) => {
2215
2340
  if (msg.error) {
2216
- const errorMessage = getApiErrorMessage(
2217
- msg.error,
2218
- "API call failed"
2219
- );
2220
- throw new Error(errorMessage);
2341
+ throw new TrueNasApiError(msg.error, method);
2221
2342
  }
2222
2343
  return msg.result;
2223
2344
  }),
@@ -2270,21 +2391,32 @@ var TrueNasApi = class {
2270
2391
  *
2271
2392
  * The id comes from the first job event whose `message_ids` carries this
2272
2393
  * request's id, not from the response: what a job method returns on the wire
2273
- * differs by version (v25.10 answers with the id, v26 with `null`), while
2394
+ * differs by version (v25.10 answers with the id, v27 with `null`), while
2274
2395
  * the event correlation holds for both.
2275
2396
  *
2276
- * @returns Observable that emits the job ID when received from websocket events
2397
+ * @returns the job's id; errors with a {@link TrueNasApiError} if the method is refused before a job starts
2277
2398
  */
2278
2399
  callAndGetJobId(method, ...params) {
2279
2400
  return defer(() => {
2280
2401
  const message = createJsonRpcMessage(method, params[0]);
2281
- const seen = this.jobEvents.pipe(
2402
+ const started = this.jobEvents.pipe(
2282
2403
  filter((job) => job.message_ids?.includes(message.id ?? "") ?? false),
2283
- map((job) => job.id),
2284
- take(1)
2404
+ map((job) => job.id)
2405
+ );
2406
+ const refused = this.connection.messages().pipe(
2407
+ withId(message.id ?? ""),
2408
+ map((reply) => {
2409
+ if (reply.error) {
2410
+ throw new TrueNasApiError(reply.error, method);
2411
+ }
2412
+ }),
2413
+ ignoreElements()
2285
2414
  );
2286
2415
  const sending = this.connection.send(message);
2287
- return seen.pipe(finalize(() => sending.unsubscribe()));
2416
+ return merge(started, refused).pipe(
2417
+ take(1),
2418
+ finalize(() => sending.unsubscribe())
2419
+ );
2288
2420
  });
2289
2421
  }
2290
2422
  /**
@@ -2303,41 +2435,118 @@ var TrueNasApi = class {
2303
2435
  switchMap((jobId) => this.trackJob(jobId))
2304
2436
  );
2305
2437
  }
2306
- /**
2307
- * Subscribe to a collection and emit its changes, discriminated on `msg`.
2308
- *
2309
- * ```typescript
2310
- * api.events('app.query').subscribe(event => {
2311
- * if (event.msg === 'removed') return drop(event.id);
2312
- * render(event.fields); // only reachable once narrowed
2313
- * });
2314
- * ```
2315
- *
2316
- * Narrow before reading `fields`: `removed` events almost never carry it.
2317
- * Sources taking subscribe-time arguments are excluded; see {@link EventName}.
2318
- */
2319
- events(event) {
2320
- const existing = this.eventStreams.get(event);
2438
+ events(event, params) {
2439
+ const name = subscriptionName(event, params);
2440
+ this.listenForEndNotices();
2441
+ const existing = this.eventStreams.get(name);
2321
2442
  if (existing) return existing;
2443
+ const forget = () => {
2444
+ if (this.eventStreams.get(name) === stream) this.eventStreams.delete(name);
2445
+ };
2322
2446
  const stream = defer(() => {
2323
- const resubscribe = this.authenticated.pipe(distinctUntilChanged(), filter(Boolean)).subscribe(() => {
2324
- this.connection.send(createJsonRpcMessage("core.subscribe", [event]));
2447
+ let subscriptionId;
2448
+ let released = false;
2449
+ let serverEnded = false;
2450
+ const ended = new Subject();
2451
+ this.serverEnds.set(name, ended);
2452
+ const subscribing = this.authenticated.pipe(distinctUntilChanged()).subscribe((isAuthenticated) => {
2453
+ subscriptionId = void 0;
2454
+ if (!isAuthenticated) return;
2455
+ const request = createJsonRpcMessage("core.subscribe", [name]);
2456
+ this.connection.messages().pipe(
2457
+ withId(request.id ?? ""),
2458
+ take(1),
2459
+ takeUntil(this.authenticated.pipe(filter((isUp) => !isUp)))
2460
+ ).subscribe((reply) => {
2461
+ if (typeof reply.result !== "string") return;
2462
+ if (!released) subscriptionId = reply.result;
2463
+ else if (!serverEnded) this.unsubscribe(name, reply.result);
2464
+ });
2465
+ this.connection.send(request);
2325
2466
  });
2326
- return this.connection.messages().pipe(
2327
- filter((res) => res.method === "collection_update"),
2328
- map((res) => res.params),
2467
+ const updates = this.connection.messages().pipe(
2329
2468
  filter(
2330
- (params) => params?.collection === event && EVENT_KINDS.includes(params.msg)
2469
+ (res) => res.method === "collection_update" && res.params?.collection === name
2331
2470
  ),
2471
+ map((res) => res.params),
2472
+ filter((params2) => EVENT_KINDS.includes(params2.msg)),
2473
+ // `merge` below ends only when both inputs do. The connection closing
2474
+ // for good ends this one; `ended` has to follow, or the stream never
2475
+ // completes and nothing tears down the resubscription.
2476
+ finalize(() => ended.complete())
2477
+ );
2478
+ const end = ended.pipe(
2479
+ map((notice) => {
2480
+ subscriptionId = void 0;
2481
+ serverEnded = true;
2482
+ if (notice.error) {
2483
+ throw new TrueNasApiError(
2484
+ { code: -32001, message: notice.error.reason, data: notice.error },
2485
+ name
2486
+ );
2487
+ }
2488
+ })
2489
+ );
2490
+ return merge(updates, end).pipe(
2491
+ takeWhile((change) => change !== void 0),
2332
2492
  // `collection` is the subscription the caller already named; what is
2333
2493
  // left is exactly the payload the directory declares, plus its tag.
2334
2494
  map(({ collection, ...change }) => change),
2335
- finalize(() => resubscribe.unsubscribe())
2495
+ // An ended stream stays ended (below), so the next `events()` call for
2496
+ // this name needs a new one.
2497
+ tap({ complete: forget, error: forget }),
2498
+ finalize(() => {
2499
+ released = true;
2500
+ subscribing.unsubscribe();
2501
+ if (this.serverEnds.get(name) === ended) this.serverEnds.delete(name);
2502
+ if (subscriptionId) this.unsubscribe(name, subscriptionId);
2503
+ })
2336
2504
  );
2337
- }).pipe(share({ resetOnRefCountZero: false, resetOnComplete: false }));
2338
- this.eventStreams.set(event, stream);
2505
+ }).pipe(
2506
+ share({
2507
+ resetOnRefCountZero: true,
2508
+ resetOnComplete: false,
2509
+ resetOnError: false
2510
+ })
2511
+ );
2512
+ this.eventStreams.set(name, stream);
2339
2513
  return stream;
2340
2514
  }
2515
+ /**
2516
+ * Release a subscription by the id `core.subscribe` answered with, holding
2517
+ * its notice window (see {@link releasing}) open until the reply.
2518
+ */
2519
+ unsubscribe(name, subscriptionId) {
2520
+ const release = { noticed: false };
2521
+ this.releasing.set(name, [...this.releasing.get(name) ?? [], release]);
2522
+ const request = createJsonRpcMessage("core.unsubscribe", [subscriptionId]);
2523
+ this.connection.messages().pipe(
2524
+ withId(request.id ?? ""),
2525
+ take(1),
2526
+ takeUntil(this.authenticated.pipe(filter((isUp) => !isUp))),
2527
+ finalize(() => {
2528
+ const open = (this.releasing.get(name) ?? []).filter((r) => r !== release);
2529
+ if (open.length) this.releasing.set(name, open);
2530
+ else this.releasing.delete(name);
2531
+ })
2532
+ ).subscribe();
2533
+ this.connection.send(request);
2534
+ }
2535
+ /**
2536
+ * Route each `notify_unsubscribed` once: an error-free one inside an open
2537
+ * release window is that release's, anything else ends the name's live
2538
+ * subscription. Installed on the first `events()` call, before any stream
2539
+ * listens.
2540
+ */
2541
+ listenForEndNotices() {
2542
+ if (this.endNotices) return;
2543
+ this.endNotices = this.connection.messages().pipe(filter((res) => res.method === "notify_unsubscribed")).subscribe((res) => {
2544
+ const notice = res.params;
2545
+ const release = notice.error ? void 0 : this.releasing.get(notice.collection)?.find((r) => !r.noticed);
2546
+ if (release) release.noticed = true;
2547
+ else this.serverEnds.get(notice.collection)?.next(notice);
2548
+ });
2549
+ }
2341
2550
  /**
2342
2551
  * Convenience wrapper for auth.generate_token API call.
2343
2552
  *
@@ -2370,12 +2579,10 @@ var TrueNasApi = class {
2370
2579
  filter((job) => job.id === jobId),
2371
2580
  map((job) => job)
2372
2581
  );
2373
- const read$ = this.dispatch("core.get_jobs", [
2374
- [["id", "=", jobId]]
2375
- ]).pipe(
2376
- map((jobs) => jobs[0]),
2377
- share()
2582
+ const readJob = () => this.dispatch("core.get_jobs", [[["id", "=", jobId]]]).pipe(
2583
+ map((jobs) => jobs[0])
2378
2584
  );
2585
+ const read$ = readJob().pipe(share());
2379
2586
  const missing$ = read$.pipe(
2380
2587
  filter((job) => job === void 0),
2381
2588
  takeUntil(updates$)
@@ -2384,10 +2591,29 @@ var TrueNasApi = class {
2384
2591
  filter((job) => job !== void 0),
2385
2592
  takeUntil(updates$)
2386
2593
  );
2387
- return merge(snapshot$, updates$).pipe(
2594
+ const reread$ = this.authenticated.pipe(
2595
+ distinctUntilChanged(),
2596
+ skipWhile((isUp) => !isUp),
2597
+ pairwise(),
2598
+ filter(([wasUp, isUp]) => !wasUp && isUp),
2599
+ switchMap(
2600
+ () => readJob().pipe(
2601
+ // Stale once events flow again, as the opening read is; they carry the end.
2602
+ takeUntil(updates$),
2603
+ // A failed read waits for the next reconnect.
2604
+ catchError(() => EMPTY)
2605
+ )
2606
+ ),
2607
+ share()
2608
+ );
2609
+ const gone$ = reread$.pipe(filter((job) => job === void 0));
2610
+ const rereadJob$ = reread$.pipe(
2611
+ filter((job) => job !== void 0)
2612
+ );
2613
+ return merge(snapshot$, updates$, rereadJob$).pipe(
2388
2614
  takeWhile((job) => !isJobFinished(job), true),
2389
2615
  // Include the final state
2390
- takeUntil(missing$)
2616
+ takeUntil(merge(missing$, gone$))
2391
2617
  );
2392
2618
  }
2393
2619
  /**
@@ -2427,7 +2653,7 @@ var apiVersionConfig = {
2427
2653
  * not write its client, so the ceiling is the newest version in
2428
2654
  * `CLIENT_BY_VERSION_KEY`. A factory test asserts the two agree.
2429
2655
  */
2430
- MAX_SUPPORTED_VERSION: "v27.0.0",
2656
+ MAX_SUPPORTED_VERSION: "v28.0.0",
2431
2657
  /**
2432
2658
  * Version assumed when discovery is CORS-blocked, as v25.10.0's
2433
2659
  * `/api/versions` is. Used only for an appliance that answers a reachability
@@ -2487,6 +2713,9 @@ function checkVersionCompatibility(version) {
2487
2713
  if (compareVersions(version, maxVersion) > 0) {
2488
2714
  return "too-new" /* TooNew */;
2489
2715
  }
2716
+ if (!SUPPORTED_API_VERSIONS.includes(version.version)) {
2717
+ return "unsupported" /* Unsupported */;
2718
+ }
2490
2719
  return "compatible" /* Compatible */;
2491
2720
  }
2492
2721
  function isVersionSupported(version) {
@@ -2592,7 +2821,7 @@ var TrueNasAuthenticator = class _TrueNasAuthenticator {
2592
2821
  /**
2593
2822
  * `login_options` asking for a reconnect token, when the server understands it.
2594
2823
  *
2595
- * Omitted below v26. `AuthCommonOptions` is `additionalProperties: false`
2824
+ * Omitted below v27. `AuthCommonOptions` is `additionalProperties: false`
2596
2825
  * there and has only `user_info`, so sending the member is a validation
2597
2826
  * error, not an ignored field — it would fail login outright on the oldest
2598
2827
  * version this client supports.
@@ -2600,7 +2829,7 @@ var TrueNasAuthenticator = class _TrueNasAuthenticator {
2600
2829
  /**
2601
2830
  * Two things this does not do, both deliberate and both worth knowing.
2602
2831
  *
2603
- * There is no way for a consumer to decline: every v26+ password login now
2832
+ * There is no way for a consumer to decline: every v27+ password login now
2604
2833
  * mints a single-use credential carrying that session's roles, whether or not
2605
2834
  * the caller wants one. And the auto-relogin in the constructor subscribes
2606
2835
  * with no observer, so the token it mints is dropped — a caller reconnecting
@@ -2735,7 +2964,7 @@ var TrueNasAuthenticator = class _TrueNasAuthenticator {
2735
2964
  * e.g. to open another connection without asking for the password again.
2736
2965
  *
2737
2966
  * Tokens are single-use, short-lived, and voided by a `middlewared` restart.
2738
- * On v26+ each successful login mints a new one; store the newest. Token
2967
+ * On v27+ each successful login mints a new one; store the newest. Token
2739
2968
  * sessions are not re-logged automatically on reconnect — the caller must.
2740
2969
  */
2741
2970
  loginWithToken(token) {
@@ -2794,7 +3023,7 @@ var TrueNasAuthenticator = class _TrueNasAuthenticator {
2794
3023
  );
2795
3024
  }
2796
3025
  /**
2797
- * No reconnect token is requested here, though v26+ would mint one: an
3026
+ * No reconnect token is requested here, though v27+ would mint one: an
2798
3027
  * api-key session already reconnects without a prompt, since the key is held
2799
3028
  * and replayed. The token exists for the credential that cannot be.
2800
3029
  */
@@ -3457,7 +3686,7 @@ function toAppState(state) {
3457
3686
  case "STARTING":
3458
3687
  case "DEPLOYING":
3459
3688
  return "DEPLOYING" /* Deploying */;
3460
- // v25.10's freezer states are the same idea as v26's `SUSPENDED`: the
3689
+ // v25.10's freezer states are the same idea as v27's `SUSPENDED`: the
3461
3690
  // processes are paused, not exited. `FROZEN` is that pause completed, so it
3462
3691
  // and `SUSPENDED` are the same answer.
3463
3692
  case "SUSPENDED":
@@ -3569,14 +3798,14 @@ function toContainer(instance) {
3569
3798
  // declares only `description`, but the API returns `architecture`, `os`,
3570
3799
  // `release` and more, and callers were already receiving them. Narrowing
3571
3800
  // to the declared field would take data away to match a type that was
3572
- // always an under-declaration — the same call as the v26 `description`
3801
+ // always an under-declaration — the same call as the v27 `description`
3573
3802
  // widening, decided the same way.
3574
3803
  image: instance.image.description === null ? void 0 : { ...instance.image, description: instance.image.description }
3575
3804
  };
3576
3805
  }
3577
- var TrueNasApiClientV26 = class extends TrueNasApiClient {
3806
+ var TrueNasApiClientV27 = class extends TrueNasApiClient {
3578
3807
  /**
3579
- * Create v26-specific operation mappings
3808
+ * Create v27-specific operation mappings
3580
3809
  *
3581
3810
  * Operations return Observable<Job | null>:
3582
3811
  * - Async operations emit Job updates until complete
@@ -3590,7 +3819,7 @@ var TrueNasApiClientV26 = class extends TrueNasApiClient {
3590
3819
  containerQuery: () => this.api.query("container.query").pipe(
3591
3820
  map((containers) => containers.map(toContainer2))
3592
3821
  ),
3593
- // container.start is synchronous in v26.0.0 - emit null
3822
+ // container.start is synchronous in v27.0.0 - emit null
3594
3823
  containerStart: (id) => this.api.call("container.start", [parseInt(id, 10)]).pipe(map(() => null)),
3595
3824
  // container.stop emits job updates
3596
3825
  containerStop: (id, options) => this.api.job("container.stop", [
@@ -3600,7 +3829,7 @@ var TrueNasApiClientV26 = class extends TrueNasApiClient {
3600
3829
  force_after_timeout: options.force
3601
3830
  }
3602
3831
  ]),
3603
- // v26.0.0 doesn't have container.restart - chain stop + start
3832
+ // v27.0.0 doesn't have container.restart - chain stop + start
3604
3833
  // Emits Job updates during stop, then null when start completes
3605
3834
  containerRestart: (id, options) => {
3606
3835
  const numericId = parseInt(id, 10);
@@ -3652,9 +3881,9 @@ function toContainer2(container) {
3652
3881
  description
3653
3882
  };
3654
3883
  }
3655
- var TrueNasApiClientV27 = class extends TrueNasApiClient {
3884
+ var TrueNasApiClientV28 = class extends TrueNasApiClient {
3656
3885
  /**
3657
- * Create v27-specific operation mappings
3886
+ * Create v28-specific operation mappings
3658
3887
  *
3659
3888
  * Operations return Observable<Job | null>:
3660
3889
  * - Async operations emit Job updates until complete
@@ -3668,7 +3897,7 @@ var TrueNasApiClientV27 = class extends TrueNasApiClient {
3668
3897
  containerQuery: () => this.api.query("container.query").pipe(
3669
3898
  map((containers) => containers.map(toContainer3))
3670
3899
  ),
3671
- // container.start is synchronous in v27 - emit null
3900
+ // container.start is synchronous in v28 - emit null
3672
3901
  containerStart: (id) => this.api.call("container.start", [parseInt(id, 10)]).pipe(map(() => null)),
3673
3902
  // container.stop emits job updates
3674
3903
  containerStop: (id, options) => this.api.job("container.stop", [
@@ -3678,7 +3907,7 @@ var TrueNasApiClientV27 = class extends TrueNasApiClient {
3678
3907
  force_after_timeout: options.force
3679
3908
  }
3680
3909
  ]),
3681
- // v27 still has no container.restart - chain stop + start.
3910
+ // v28 still has no container.restart - chain stop + start.
3682
3911
  // Emits Job updates during stop, then null when start completes.
3683
3912
  containerRestart: (id, options) => {
3684
3913
  const numericId = parseInt(id, 10);
@@ -3700,7 +3929,7 @@ var TrueNasApiClientV27 = class extends TrueNasApiClient {
3700
3929
  )
3701
3930
  );
3702
3931
  },
3703
- // Absent options are omitted, as in v26: `[id, null]` fails validation.
3932
+ // Absent options are omitted, as in v27: `[id, null]` fails validation.
3704
3933
  containerDelete: (id, options) => this.api.job(
3705
3934
  "container.delete",
3706
3935
  options ? [parseInt(id, 10), options] : [parseInt(id, 10)]
@@ -3797,13 +4026,16 @@ var NoCompatibleVersionsError = class extends VersionDiscoveryError {
3797
4026
  constructor(hostname, availableVersions, supportedRange = {
3798
4027
  min: apiVersionConfig.MIN_SUPPORTED_VERSION,
3799
4028
  max: apiVersionConfig.MAX_SUPPORTED_VERSION
3800
- }) {
4029
+ }, supportedVersions = SUPPORTED_API_VERSIONS) {
3801
4030
  super(
3802
- `No compatible API versions available. Supported: ${supportedRange.min} to ${supportedRange.max}. Available: ${availableVersions.join(", ")}`,
4031
+ `No compatible API versions available. Supported: ${supportedVersions.join(
4032
+ ", "
4033
+ )}. Available: ${availableVersions.join(", ")}`,
3803
4034
  hostname
3804
4035
  );
3805
4036
  this.availableVersions = availableVersions;
3806
4037
  this.supportedRange = supportedRange;
4038
+ this.supportedVersions = supportedVersions;
3807
4039
  }
3808
4040
  };
3809
4041
  var InvalidVersionResponseError = class extends VersionDiscoveryError {
@@ -4222,8 +4454,8 @@ function selectRepresentativeFailure(failures) {
4222
4454
  }
4223
4455
  var CLIENT_BY_VERSION_KEY = {
4224
4456
  "25.10": TrueNasApiClientV2510,
4225
- "26": TrueNasApiClientV26,
4226
- "27": TrueNasApiClientV27
4457
+ "27": TrueNasApiClientV27,
4458
+ "28": TrueNasApiClientV28
4227
4459
  };
4228
4460
  function clientVersionKey(version) {
4229
4461
  if (version.year <= legacyCutoffYear) {
@@ -4280,6 +4512,6 @@ var UserRole = /* @__PURE__ */ ((UserRole2) => {
4280
4512
  return UserRole2;
4281
4513
  })(UserRole || {});
4282
4514
 
4283
- export { AppState, AuthError, AuthErrorCode, AuthResponseType, InvalidVersionResponseError, JobState, NoCompatibleVersionsError, SUPPORTED_API_VERSIONS, TrueNasApiClient, TrueNasApiClientV2510, TrueNasApiClientV26, TrueNasApiClientV27, TrueNasAuthMechanism, TrueNasAuthenticator, TrueNasConnection, UserRole, VersionCompatibility, VersionDiscovery, VersionDiscoveryError, VersionDiscoveryNetworkError, VersionDiscoveryTimeoutError, VersionEndpointNotFoundError, VersionTooNewError, VersionTooOldError, clientClassFor, consoleLogger, createTrueNasClient, getApiErrorMessage, getCloseMessage, isJobFinished, noopLogger, parseApiVersion, policyViolationCloseCode, v25_10_0_exports, v25_10_1_exports, v25_10_2_exports, v25_10_3_exports, v25_10_4_exports, v25_10_5_exports, v26_0_0_exports, v27_0_0_exports };
4284
- //# sourceMappingURL=chunk-RH7UXULE.js.map
4285
- //# sourceMappingURL=chunk-RH7UXULE.js.map
4515
+ 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 };
4516
+ //# sourceMappingURL=chunk-K7BL6J3Z.js.map
4517
+ //# sourceMappingURL=chunk-K7BL6J3Z.js.map