@truenas/api-client 9.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.
- package/README.md +33 -4
- package/dist/{chunk-LFNAVARU.js → chunk-K7BL6J3Z.js} +205 -49
- package/dist/chunk-K7BL6J3Z.js.map +1 -0
- package/dist/{chunk-TSO2EHF3.cjs → chunk-U6V2Y3AE.cjs} +205 -47
- package/dist/chunk-U6V2Y3AE.cjs.map +1 -0
- package/dist/index.cjs +40 -36
- package/dist/index.d.cts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +1 -1
- package/dist/testing/index.cjs +28 -16
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +15 -2
- package/dist/testing/index.d.ts +15 -2
- package/dist/testing/index.js +18 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/{truenas-auth-mechanism.enum-CW5Wwz1s.d.cts → truenas-auth-mechanism.enum-Bp2XMMwK.d.cts} +65 -18
- package/dist/{truenas-auth-mechanism.enum-CW5Wwz1s.d.ts → truenas-auth-mechanism.enum-Bp2XMMwK.d.ts} +65 -18
- package/package.json +2 -2
- package/dist/chunk-LFNAVARU.js.map +0 -1
- package/dist/chunk-TSO2EHF3.cjs.map +0 -1
|
@@ -30,6 +30,34 @@ function getApiErrorMessage(error, fallback = "API call failed") {
|
|
|
30
30
|
return fallback;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/errors/api.errors.ts
|
|
34
|
+
var TrueNasApiError = class _TrueNasApiError extends Error {
|
|
35
|
+
constructor(frame, method) {
|
|
36
|
+
super(getApiErrorMessage(frame, "API call failed"));
|
|
37
|
+
this.frame = frame;
|
|
38
|
+
this.method = method;
|
|
39
|
+
this.name = "TrueNasApiError";
|
|
40
|
+
Object.setPrototypeOf(this, _TrueNasApiError.prototype);
|
|
41
|
+
}
|
|
42
|
+
/** The JSON-RPC code: `-32001` for a refused call, others for a malformed one. */
|
|
43
|
+
get code() {
|
|
44
|
+
return this.frame.code;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The TrueNAS payload — `errname`, `reason`, `extra`, `trace`.
|
|
48
|
+
*
|
|
49
|
+
* `undefined` for the codes middleware sends without one, such as
|
|
50
|
+
* `-32601 "Method does not exist"`.
|
|
51
|
+
*/
|
|
52
|
+
get data() {
|
|
53
|
+
return this.frame.data;
|
|
54
|
+
}
|
|
55
|
+
/** `data.errname`, e.g. `EINVAL`, or `undefined` when there is no payload. */
|
|
56
|
+
get errname() {
|
|
57
|
+
return this.frame.data?.errname;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
33
61
|
// src/types/job.type.ts
|
|
34
62
|
var JobState = /* @__PURE__ */ ((JobState2) => {
|
|
35
63
|
JobState2["Pending"] = "PENDING";
|
|
@@ -2224,6 +2252,19 @@ function createJsonRpcMessage(method, params) {
|
|
|
2224
2252
|
};
|
|
2225
2253
|
}
|
|
2226
2254
|
|
|
2255
|
+
// src/utils/subscription.utils.ts
|
|
2256
|
+
function subscriptionName(event, params) {
|
|
2257
|
+
return params === void 0 ? event : `${event}:${JSON.stringify(params, sortKeys)}`;
|
|
2258
|
+
}
|
|
2259
|
+
function sortKeys(_key, value) {
|
|
2260
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
2261
|
+
return value;
|
|
2262
|
+
}
|
|
2263
|
+
return Object.fromEntries(
|
|
2264
|
+
Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2227
2268
|
// src/api/truenas-api.ts
|
|
2228
2269
|
var EVENT_KINDS = ["added", "changed", "removed"];
|
|
2229
2270
|
var TrueNasApi = class {
|
|
@@ -2233,8 +2274,13 @@ var TrueNasApi = class {
|
|
|
2233
2274
|
/**
|
|
2234
2275
|
* Stream of job events from websocket.
|
|
2235
2276
|
* JSON-RPC 2.0 events have structure: { method: 'collection_update', params: { collection, fields, ... } }
|
|
2277
|
+
*
|
|
2278
|
+
* Reads `messages()` when first subscribed, not when this is built, as every
|
|
2279
|
+
* other verb does. A consumer that wraps the connection's `messages()` after
|
|
2280
|
+
* the client is created — a debug panel mixing in mocked answers — is
|
|
2281
|
+
* otherwise heard by `call` and `events` but not by job tracking.
|
|
2236
2282
|
*/
|
|
2237
|
-
this.jobEvents = this.connection.messages().pipe(
|
|
2283
|
+
this.jobEvents = rxjs.defer(() => this.connection.messages()).pipe(
|
|
2238
2284
|
rxjs.filter(
|
|
2239
2285
|
(res) => res.method === "collection_update" && res.params?.collection === "core.get_jobs"
|
|
2240
2286
|
),
|
|
@@ -2243,11 +2289,20 @@ var TrueNasApi = class {
|
|
|
2243
2289
|
rxjs.share()
|
|
2244
2290
|
);
|
|
2245
2291
|
/**
|
|
2246
|
-
* One shared stream per
|
|
2247
|
-
*
|
|
2248
|
-
* for lives on the server and is per socket, not per caller.
|
|
2292
|
+
* One shared stream per subscription — see {@link events}. Keyed by the
|
|
2293
|
+
* name *with* its arguments: a tail of one file is not a tail of another.
|
|
2249
2294
|
*/
|
|
2250
2295
|
this.eventStreams = /* @__PURE__ */ new Map();
|
|
2296
|
+
/**
|
|
2297
|
+
* Releases awaiting their `core.unsubscribe` reply, per name. Middleware
|
|
2298
|
+
* sends a source's release notice (`terminate`) before that reply, and a
|
|
2299
|
+
* notice names only the collection; so an error-free one arriving inside
|
|
2300
|
+
* that window belongs to the release, not to a later subscription. One per
|
|
2301
|
+
* window, which closes on the reply whether or not a notice came.
|
|
2302
|
+
*/
|
|
2303
|
+
this.releasing = /* @__PURE__ */ new Map();
|
|
2304
|
+
/** The live subscription per name, told when the server ends it. */
|
|
2305
|
+
this.serverEnds = /* @__PURE__ */ new Map();
|
|
2251
2306
|
this.initializeJobEventsSubscription();
|
|
2252
2307
|
}
|
|
2253
2308
|
/**
|
|
@@ -2285,11 +2340,7 @@ var TrueNasApi = class {
|
|
|
2285
2340
|
withId(messageId),
|
|
2286
2341
|
rxjs.map((msg) => {
|
|
2287
2342
|
if (msg.error) {
|
|
2288
|
-
|
|
2289
|
-
msg.error,
|
|
2290
|
-
"API call failed"
|
|
2291
|
-
);
|
|
2292
|
-
throw new Error(errorMessage);
|
|
2343
|
+
throw new TrueNasApiError(msg.error, method);
|
|
2293
2344
|
}
|
|
2294
2345
|
return msg.result;
|
|
2295
2346
|
}),
|
|
@@ -2345,18 +2396,29 @@ var TrueNasApi = class {
|
|
|
2345
2396
|
* differs by version (v25.10 answers with the id, v27 with `null`), while
|
|
2346
2397
|
* the event correlation holds for both.
|
|
2347
2398
|
*
|
|
2348
|
-
* @returns
|
|
2399
|
+
* @returns the job's id; errors with a {@link TrueNasApiError} if the method is refused before a job starts
|
|
2349
2400
|
*/
|
|
2350
2401
|
callAndGetJobId(method, ...params) {
|
|
2351
2402
|
return rxjs.defer(() => {
|
|
2352
2403
|
const message = createJsonRpcMessage(method, params[0]);
|
|
2353
|
-
const
|
|
2404
|
+
const started = this.jobEvents.pipe(
|
|
2354
2405
|
rxjs.filter((job) => job.message_ids?.includes(message.id ?? "") ?? false),
|
|
2355
|
-
rxjs.map((job) => job.id)
|
|
2356
|
-
|
|
2406
|
+
rxjs.map((job) => job.id)
|
|
2407
|
+
);
|
|
2408
|
+
const refused = this.connection.messages().pipe(
|
|
2409
|
+
withId(message.id ?? ""),
|
|
2410
|
+
rxjs.map((reply) => {
|
|
2411
|
+
if (reply.error) {
|
|
2412
|
+
throw new TrueNasApiError(reply.error, method);
|
|
2413
|
+
}
|
|
2414
|
+
}),
|
|
2415
|
+
rxjs.ignoreElements()
|
|
2357
2416
|
);
|
|
2358
2417
|
const sending = this.connection.send(message);
|
|
2359
|
-
return
|
|
2418
|
+
return rxjs.merge(started, refused).pipe(
|
|
2419
|
+
rxjs.take(1),
|
|
2420
|
+
rxjs.finalize(() => sending.unsubscribe())
|
|
2421
|
+
);
|
|
2360
2422
|
});
|
|
2361
2423
|
}
|
|
2362
2424
|
/**
|
|
@@ -2375,41 +2437,118 @@ var TrueNasApi = class {
|
|
|
2375
2437
|
rxjs.switchMap((jobId) => this.trackJob(jobId))
|
|
2376
2438
|
);
|
|
2377
2439
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
* api.events('app.query').subscribe(event => {
|
|
2383
|
-
* if (event.msg === 'removed') return drop(event.id);
|
|
2384
|
-
* render(event.fields); // only reachable once narrowed
|
|
2385
|
-
* });
|
|
2386
|
-
* ```
|
|
2387
|
-
*
|
|
2388
|
-
* Narrow before reading `fields`: `removed` events almost never carry it.
|
|
2389
|
-
* Sources taking subscribe-time arguments are excluded; see {@link EventName}.
|
|
2390
|
-
*/
|
|
2391
|
-
events(event) {
|
|
2392
|
-
const existing = this.eventStreams.get(event);
|
|
2440
|
+
events(event, params) {
|
|
2441
|
+
const name = subscriptionName(event, params);
|
|
2442
|
+
this.listenForEndNotices();
|
|
2443
|
+
const existing = this.eventStreams.get(name);
|
|
2393
2444
|
if (existing) return existing;
|
|
2445
|
+
const forget = () => {
|
|
2446
|
+
if (this.eventStreams.get(name) === stream) this.eventStreams.delete(name);
|
|
2447
|
+
};
|
|
2394
2448
|
const stream = rxjs.defer(() => {
|
|
2395
|
-
|
|
2396
|
-
|
|
2449
|
+
let subscriptionId;
|
|
2450
|
+
let released = false;
|
|
2451
|
+
let serverEnded = false;
|
|
2452
|
+
const ended = new rxjs.Subject();
|
|
2453
|
+
this.serverEnds.set(name, ended);
|
|
2454
|
+
const subscribing = this.authenticated.pipe(rxjs.distinctUntilChanged()).subscribe((isAuthenticated) => {
|
|
2455
|
+
subscriptionId = void 0;
|
|
2456
|
+
if (!isAuthenticated) return;
|
|
2457
|
+
const request = createJsonRpcMessage("core.subscribe", [name]);
|
|
2458
|
+
this.connection.messages().pipe(
|
|
2459
|
+
withId(request.id ?? ""),
|
|
2460
|
+
rxjs.take(1),
|
|
2461
|
+
rxjs.takeUntil(this.authenticated.pipe(rxjs.filter((isUp) => !isUp)))
|
|
2462
|
+
).subscribe((reply) => {
|
|
2463
|
+
if (typeof reply.result !== "string") return;
|
|
2464
|
+
if (!released) subscriptionId = reply.result;
|
|
2465
|
+
else if (!serverEnded) this.unsubscribe(name, reply.result);
|
|
2466
|
+
});
|
|
2467
|
+
this.connection.send(request);
|
|
2397
2468
|
});
|
|
2398
|
-
|
|
2399
|
-
rxjs.filter((res) => res.method === "collection_update"),
|
|
2400
|
-
rxjs.map((res) => res.params),
|
|
2469
|
+
const updates = this.connection.messages().pipe(
|
|
2401
2470
|
rxjs.filter(
|
|
2402
|
-
(
|
|
2471
|
+
(res) => res.method === "collection_update" && res.params?.collection === name
|
|
2403
2472
|
),
|
|
2473
|
+
rxjs.map((res) => res.params),
|
|
2474
|
+
rxjs.filter((params2) => EVENT_KINDS.includes(params2.msg)),
|
|
2475
|
+
// `merge` below ends only when both inputs do. The connection closing
|
|
2476
|
+
// for good ends this one; `ended` has to follow, or the stream never
|
|
2477
|
+
// completes and nothing tears down the resubscription.
|
|
2478
|
+
rxjs.finalize(() => ended.complete())
|
|
2479
|
+
);
|
|
2480
|
+
const end = ended.pipe(
|
|
2481
|
+
rxjs.map((notice) => {
|
|
2482
|
+
subscriptionId = void 0;
|
|
2483
|
+
serverEnded = true;
|
|
2484
|
+
if (notice.error) {
|
|
2485
|
+
throw new TrueNasApiError(
|
|
2486
|
+
{ code: -32001, message: notice.error.reason, data: notice.error },
|
|
2487
|
+
name
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
})
|
|
2491
|
+
);
|
|
2492
|
+
return rxjs.merge(updates, end).pipe(
|
|
2493
|
+
rxjs.takeWhile((change) => change !== void 0),
|
|
2404
2494
|
// `collection` is the subscription the caller already named; what is
|
|
2405
2495
|
// left is exactly the payload the directory declares, plus its tag.
|
|
2406
2496
|
rxjs.map(({ collection, ...change }) => change),
|
|
2407
|
-
|
|
2497
|
+
// An ended stream stays ended (below), so the next `events()` call for
|
|
2498
|
+
// this name needs a new one.
|
|
2499
|
+
rxjs.tap({ complete: forget, error: forget }),
|
|
2500
|
+
rxjs.finalize(() => {
|
|
2501
|
+
released = true;
|
|
2502
|
+
subscribing.unsubscribe();
|
|
2503
|
+
if (this.serverEnds.get(name) === ended) this.serverEnds.delete(name);
|
|
2504
|
+
if (subscriptionId) this.unsubscribe(name, subscriptionId);
|
|
2505
|
+
})
|
|
2408
2506
|
);
|
|
2409
|
-
}).pipe(
|
|
2410
|
-
|
|
2507
|
+
}).pipe(
|
|
2508
|
+
rxjs.share({
|
|
2509
|
+
resetOnRefCountZero: true,
|
|
2510
|
+
resetOnComplete: false,
|
|
2511
|
+
resetOnError: false
|
|
2512
|
+
})
|
|
2513
|
+
);
|
|
2514
|
+
this.eventStreams.set(name, stream);
|
|
2411
2515
|
return stream;
|
|
2412
2516
|
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Release a subscription by the id `core.subscribe` answered with, holding
|
|
2519
|
+
* its notice window (see {@link releasing}) open until the reply.
|
|
2520
|
+
*/
|
|
2521
|
+
unsubscribe(name, subscriptionId) {
|
|
2522
|
+
const release = { noticed: false };
|
|
2523
|
+
this.releasing.set(name, [...this.releasing.get(name) ?? [], release]);
|
|
2524
|
+
const request = createJsonRpcMessage("core.unsubscribe", [subscriptionId]);
|
|
2525
|
+
this.connection.messages().pipe(
|
|
2526
|
+
withId(request.id ?? ""),
|
|
2527
|
+
rxjs.take(1),
|
|
2528
|
+
rxjs.takeUntil(this.authenticated.pipe(rxjs.filter((isUp) => !isUp))),
|
|
2529
|
+
rxjs.finalize(() => {
|
|
2530
|
+
const open = (this.releasing.get(name) ?? []).filter((r) => r !== release);
|
|
2531
|
+
if (open.length) this.releasing.set(name, open);
|
|
2532
|
+
else this.releasing.delete(name);
|
|
2533
|
+
})
|
|
2534
|
+
).subscribe();
|
|
2535
|
+
this.connection.send(request);
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Route each `notify_unsubscribed` once: an error-free one inside an open
|
|
2539
|
+
* release window is that release's, anything else ends the name's live
|
|
2540
|
+
* subscription. Installed on the first `events()` call, before any stream
|
|
2541
|
+
* listens.
|
|
2542
|
+
*/
|
|
2543
|
+
listenForEndNotices() {
|
|
2544
|
+
if (this.endNotices) return;
|
|
2545
|
+
this.endNotices = this.connection.messages().pipe(rxjs.filter((res) => res.method === "notify_unsubscribed")).subscribe((res) => {
|
|
2546
|
+
const notice = res.params;
|
|
2547
|
+
const release = notice.error ? void 0 : this.releasing.get(notice.collection)?.find((r) => !r.noticed);
|
|
2548
|
+
if (release) release.noticed = true;
|
|
2549
|
+
else this.serverEnds.get(notice.collection)?.next(notice);
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2413
2552
|
/**
|
|
2414
2553
|
* Convenience wrapper for auth.generate_token API call.
|
|
2415
2554
|
*
|
|
@@ -2442,12 +2581,10 @@ var TrueNasApi = class {
|
|
|
2442
2581
|
rxjs.filter((job) => job.id === jobId),
|
|
2443
2582
|
rxjs.map((job) => job)
|
|
2444
2583
|
);
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
]).pipe(
|
|
2448
|
-
rxjs.map((jobs) => jobs[0]),
|
|
2449
|
-
rxjs.share()
|
|
2584
|
+
const readJob = () => this.dispatch("core.get_jobs", [[["id", "=", jobId]]]).pipe(
|
|
2585
|
+
rxjs.map((jobs) => jobs[0])
|
|
2450
2586
|
);
|
|
2587
|
+
const read$ = readJob().pipe(rxjs.share());
|
|
2451
2588
|
const missing$ = read$.pipe(
|
|
2452
2589
|
rxjs.filter((job) => job === void 0),
|
|
2453
2590
|
rxjs.takeUntil(updates$)
|
|
@@ -2456,10 +2593,29 @@ var TrueNasApi = class {
|
|
|
2456
2593
|
rxjs.filter((job) => job !== void 0),
|
|
2457
2594
|
rxjs.takeUntil(updates$)
|
|
2458
2595
|
);
|
|
2459
|
-
|
|
2596
|
+
const reread$ = this.authenticated.pipe(
|
|
2597
|
+
rxjs.distinctUntilChanged(),
|
|
2598
|
+
rxjs.skipWhile((isUp) => !isUp),
|
|
2599
|
+
rxjs.pairwise(),
|
|
2600
|
+
rxjs.filter(([wasUp, isUp]) => !wasUp && isUp),
|
|
2601
|
+
rxjs.switchMap(
|
|
2602
|
+
() => readJob().pipe(
|
|
2603
|
+
// Stale once events flow again, as the opening read is; they carry the end.
|
|
2604
|
+
rxjs.takeUntil(updates$),
|
|
2605
|
+
// A failed read waits for the next reconnect.
|
|
2606
|
+
rxjs.catchError(() => rxjs.EMPTY)
|
|
2607
|
+
)
|
|
2608
|
+
),
|
|
2609
|
+
rxjs.share()
|
|
2610
|
+
);
|
|
2611
|
+
const gone$ = reread$.pipe(rxjs.filter((job) => job === void 0));
|
|
2612
|
+
const rereadJob$ = reread$.pipe(
|
|
2613
|
+
rxjs.filter((job) => job !== void 0)
|
|
2614
|
+
);
|
|
2615
|
+
return rxjs.merge(snapshot$, updates$, rereadJob$).pipe(
|
|
2460
2616
|
rxjs.takeWhile((job) => !isJobFinished(job), true),
|
|
2461
2617
|
// Include the final state
|
|
2462
|
-
rxjs.takeUntil(missing$)
|
|
2618
|
+
rxjs.takeUntil(rxjs.merge(missing$, gone$))
|
|
2463
2619
|
);
|
|
2464
2620
|
}
|
|
2465
2621
|
/**
|
|
@@ -4370,6 +4526,7 @@ exports.TrueNasApiClient = TrueNasApiClient;
|
|
|
4370
4526
|
exports.TrueNasApiClientV2510 = TrueNasApiClientV2510;
|
|
4371
4527
|
exports.TrueNasApiClientV27 = TrueNasApiClientV27;
|
|
4372
4528
|
exports.TrueNasApiClientV28 = TrueNasApiClientV28;
|
|
4529
|
+
exports.TrueNasApiError = TrueNasApiError;
|
|
4373
4530
|
exports.TrueNasAuthMechanism = TrueNasAuthMechanism;
|
|
4374
4531
|
exports.TrueNasAuthenticator = TrueNasAuthenticator;
|
|
4375
4532
|
exports.TrueNasConnection = TrueNasConnection;
|
|
@@ -4391,6 +4548,7 @@ exports.isJobFinished = isJobFinished;
|
|
|
4391
4548
|
exports.noopLogger = noopLogger;
|
|
4392
4549
|
exports.parseApiVersion = parseApiVersion;
|
|
4393
4550
|
exports.policyViolationCloseCode = policyViolationCloseCode;
|
|
4551
|
+
exports.subscriptionName = subscriptionName;
|
|
4394
4552
|
exports.v25_10_0_exports = v25_10_0_exports;
|
|
4395
4553
|
exports.v25_10_1_exports = v25_10_1_exports;
|
|
4396
4554
|
exports.v25_10_2_exports = v25_10_2_exports;
|
|
@@ -4399,5 +4557,5 @@ exports.v25_10_4_exports = v25_10_4_exports;
|
|
|
4399
4557
|
exports.v25_10_5_exports = v25_10_5_exports;
|
|
4400
4558
|
exports.v27_0_0_exports = v27_0_0_exports;
|
|
4401
4559
|
exports.v28_0_0_exports = v28_0_0_exports;
|
|
4402
|
-
//# sourceMappingURL=chunk-
|
|
4403
|
-
//# sourceMappingURL=chunk-
|
|
4560
|
+
//# sourceMappingURL=chunk-U6V2Y3AE.cjs.map
|
|
4561
|
+
//# sourceMappingURL=chunk-U6V2Y3AE.cjs.map
|