@truenas/api-client 9.0.4 → 9.0.6
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 +15 -0
- package/dist/{chunk-PEOXWWBS.js → chunk-R4VP3HBB.js} +243 -6
- package/dist/chunk-R4VP3HBB.js.map +1 -0
- package/dist/{chunk-WAVCPALL.cjs → chunk-UOTSHE2J.cjs} +243 -6
- package/dist/chunk-UOTSHE2J.cjs.map +1 -0
- package/dist/index.cjs +37 -37
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/testing/index.cjs +13 -13
- package/dist/testing/index.d.cts +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{truenas-auth-mechanism.enum-Djr9ulY7.d.cts → truenas-auth-mechanism.enum-CtU2b15s.d.cts} +71 -18
- package/dist/{truenas-auth-mechanism.enum-Djr9ulY7.d.ts → truenas-auth-mechanism.enum-CtU2b15s.d.ts} +71 -18
- package/package.json +1 -1
- package/dist/chunk-PEOXWWBS.js.map +0 -1
- package/dist/chunk-WAVCPALL.cjs.map +0 -1
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
|
|
@@ -3175,6 +3175,157 @@ function getWebSocketError(code) {
|
|
|
3175
3175
|
return "Network connection failed - Check IP address and connectivity";
|
|
3176
3176
|
}
|
|
3177
3177
|
}
|
|
3178
|
+
var CallLimiter = class {
|
|
3179
|
+
constructor(limit, write) {
|
|
3180
|
+
this.limit = limit;
|
|
3181
|
+
this.write = write;
|
|
3182
|
+
/**
|
|
3183
|
+
* Each request awaiting an answer, with the handle `write` gave for it. That
|
|
3184
|
+
* handle closes once the frame is on a socket: until then the connection is
|
|
3185
|
+
* still holding it for one, and giving up on it takes it back.
|
|
3186
|
+
*/
|
|
3187
|
+
this.inFlight = /* @__PURE__ */ new Map();
|
|
3188
|
+
this.waiting = [];
|
|
3189
|
+
/**
|
|
3190
|
+
* Calls that were waiting when a socket was lost, kept apart from `waiting`
|
|
3191
|
+
* until `release()`. A call sent after the loss, the sign-in among them, then
|
|
3192
|
+
* never waits behind one: nothing would write it, and `release()` waits on it.
|
|
3193
|
+
*/
|
|
3194
|
+
this.heldCalls = [];
|
|
3195
|
+
}
|
|
3196
|
+
/** Requests written or being written, and not answered yet. */
|
|
3197
|
+
get inFlightCount() {
|
|
3198
|
+
return this.inFlight.size;
|
|
3199
|
+
}
|
|
3200
|
+
/** Requests waiting for a place, held ones included. */
|
|
3201
|
+
get waitingCount() {
|
|
3202
|
+
return this.waiting.length + this.heldCalls.length;
|
|
3203
|
+
}
|
|
3204
|
+
/** Whether calls are being kept from a lost socket until `release()`. */
|
|
3205
|
+
get held() {
|
|
3206
|
+
return this.heldCalls.length > 0;
|
|
3207
|
+
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Sends `message` now if there is room, or queues it. Frames that are not
|
|
3210
|
+
* requests pass straight through. Unsubscribing the returned handle takes
|
|
3211
|
+
* the call back if it has not been written yet.
|
|
3212
|
+
*
|
|
3213
|
+
* New calls are not held back by a lost socket, only by the limit: the frames
|
|
3214
|
+
* that sign a new socket in come through here, and the held calls are
|
|
3215
|
+
* waiting on exactly those.
|
|
3216
|
+
*/
|
|
3217
|
+
send(message) {
|
|
3218
|
+
if (!isRequest(message)) {
|
|
3219
|
+
return this.write(message);
|
|
3220
|
+
}
|
|
3221
|
+
const call = { id: message.id, message, handle: new Subscription() };
|
|
3222
|
+
if (this.inFlight.size < this.limit) {
|
|
3223
|
+
this.start(call);
|
|
3224
|
+
return call.handle;
|
|
3225
|
+
}
|
|
3226
|
+
this.waiting.push(call);
|
|
3227
|
+
call.handle.add(() => {
|
|
3228
|
+
remove(this.waiting, call);
|
|
3229
|
+
remove(this.heldCalls, call);
|
|
3230
|
+
});
|
|
3231
|
+
return call.handle;
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Counts a request the connection wrote to a socket itself, such as its
|
|
3235
|
+
* keepalive `core.ping`, or returns `false` without counting it when there is
|
|
3236
|
+
* no room. A request written that way is not the limiter's to queue: the
|
|
3237
|
+
* caller decides what not sending it means.
|
|
3238
|
+
*/
|
|
3239
|
+
tryTrack(id) {
|
|
3240
|
+
if (this.inFlight.size >= this.limit) {
|
|
3241
|
+
return false;
|
|
3242
|
+
}
|
|
3243
|
+
this.inFlight.set(id, Subscription.EMPTY);
|
|
3244
|
+
return true;
|
|
3245
|
+
}
|
|
3246
|
+
/** Frees the place of the request this frame answers, if it is one. */
|
|
3247
|
+
received(message) {
|
|
3248
|
+
if (message.id === void 0 || message.id === null || message.method) {
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
this.finish(String(message.id));
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* What was written on the lost socket is lost with it, answers included, so
|
|
3255
|
+
* those places are freed. A frame the connection has not written yet is still
|
|
3256
|
+
* its to send on the next socket, so it stays counted. Calls still waiting are
|
|
3257
|
+
* held until `release()`: the new socket starts signed out, and a call sent on
|
|
3258
|
+
* it before the session is back is refused.
|
|
3259
|
+
*/
|
|
3260
|
+
socketLost() {
|
|
3261
|
+
this.inFlight.forEach((sending, id) => {
|
|
3262
|
+
if (sending.closed) {
|
|
3263
|
+
this.inFlight.delete(id);
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3266
|
+
this.heldCalls.push(...this.waiting.splice(0));
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Lets held calls go out again, once the session they were made in is back.
|
|
3270
|
+
* They were sent before anything still waiting, so they go first.
|
|
3271
|
+
*/
|
|
3272
|
+
release() {
|
|
3273
|
+
this.waiting.unshift(...this.heldCalls.splice(0));
|
|
3274
|
+
this.startWaiting();
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Takes back every call held from a lost socket, unsent, and returns their
|
|
3278
|
+
* ids so the caller can answer them. Does nothing while nothing is held.
|
|
3279
|
+
*/
|
|
3280
|
+
takeHeld() {
|
|
3281
|
+
return this.heldCalls.splice(0).map(({ id }) => id);
|
|
3282
|
+
}
|
|
3283
|
+
// The three below call each other in a ring.
|
|
3284
|
+
start({ id, message, handle }) {
|
|
3285
|
+
this.inFlight.set(id, Subscription.EMPTY);
|
|
3286
|
+
let sending;
|
|
3287
|
+
try {
|
|
3288
|
+
sending = this.write(message);
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
this.finish(id);
|
|
3291
|
+
throw error;
|
|
3292
|
+
}
|
|
3293
|
+
if (!this.inFlight.has(id)) {
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
this.inFlight.set(id, sending);
|
|
3297
|
+
handle.add(() => {
|
|
3298
|
+
const isWritten = sending.closed;
|
|
3299
|
+
sending.unsubscribe();
|
|
3300
|
+
if (!isWritten && this.inFlight.get(id) === sending) {
|
|
3301
|
+
this.finish(id);
|
|
3302
|
+
}
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
startWaiting() {
|
|
3306
|
+
while (this.inFlight.size < this.limit) {
|
|
3307
|
+
const next = this.waiting.shift();
|
|
3308
|
+
if (!next) {
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
this.start(next);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
finish(id) {
|
|
3315
|
+
if (this.inFlight.delete(id)) {
|
|
3316
|
+
this.startWaiting();
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
};
|
|
3320
|
+
function remove(calls, call) {
|
|
3321
|
+
const index = calls.indexOf(call);
|
|
3322
|
+
if (index >= 0) {
|
|
3323
|
+
calls.splice(index, 1);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
function isRequest(message) {
|
|
3327
|
+
return message.id !== void 0 && message.id !== null && Boolean(message.method);
|
|
3328
|
+
}
|
|
3178
3329
|
var TrueNasSocket = class {
|
|
3179
3330
|
constructor(config) {
|
|
3180
3331
|
this.config = config;
|
|
@@ -3203,7 +3354,7 @@ function socketScheme(protocol) {
|
|
|
3203
3354
|
var tenSeconds = 10 * 1e3;
|
|
3204
3355
|
var twentySeconds = 20 * 1e3;
|
|
3205
3356
|
var TrueNasConnection = class {
|
|
3206
|
-
constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:", interceptor) {
|
|
3357
|
+
constructor(initialEnabled, hostnames, systemUuid, websocketPath, systemName, retryDelay = tenSeconds, maxRetry = 3, logger = noopLogger, protocol = "https:", interceptor, maxConcurrentCalls) {
|
|
3207
3358
|
this.systemUuid = systemUuid;
|
|
3208
3359
|
this.websocketPath = websocketPath;
|
|
3209
3360
|
this.systemName = systemName;
|
|
@@ -3211,6 +3362,7 @@ var TrueNasConnection = class {
|
|
|
3211
3362
|
this.maxRetry = maxRetry;
|
|
3212
3363
|
this.logger = logger;
|
|
3213
3364
|
this.interceptor = interceptor;
|
|
3365
|
+
this.maxConcurrentCalls = maxConcurrentCalls;
|
|
3214
3366
|
// compatibility properties
|
|
3215
3367
|
this.opened = new BehaviorSubject(false);
|
|
3216
3368
|
this.closed = new Subject();
|
|
@@ -3374,16 +3526,39 @@ var TrueNasConnection = class {
|
|
|
3374
3526
|
catchError(() => EMPTY)
|
|
3375
3527
|
))
|
|
3376
3528
|
);
|
|
3377
|
-
|
|
3529
|
+
/** Answers given here, to calls ended without being sent. See `refuseHeldCalls()`. */
|
|
3530
|
+
this.refusals$ = new Subject();
|
|
3531
|
+
if (maxConcurrentCalls !== void 0 && !(Number.isInteger(maxConcurrentCalls) && maxConcurrentCalls > 0)) {
|
|
3532
|
+
throw new Error(`maxConcurrentCalls must be a positive integer, got ${String(maxConcurrentCalls)}`);
|
|
3533
|
+
}
|
|
3534
|
+
this.limiter = maxConcurrentCalls === void 0 ? null : new CallLimiter(maxConcurrentCalls, (message) => this.writeToNextSocket(message));
|
|
3535
|
+
this.messages$ = interceptor?.injected$ ? merge(this.socketMessages$, this.refusals$, interceptor.injected$.pipe(observeOn(asapScheduler), share())) : merge(this.socketMessages$, this.refusals$);
|
|
3378
3536
|
this.endpoint$.next(resolveEndpoint(hostnames, protocol));
|
|
3379
3537
|
this.ws$.pipe(
|
|
3380
3538
|
switchMap((ws) => ws ? interval(twentySeconds).pipe(map(() => ws)) : EMPTY),
|
|
3381
3539
|
takeUntil(this.closeConnection)
|
|
3382
3540
|
).subscribe((ws) => {
|
|
3383
3541
|
const ping = createJsonRpcMessage("core.ping");
|
|
3542
|
+
if (this.limiter && !this.limiter.tryTrack(ping.id ?? "")) {
|
|
3543
|
+
this.logger.debug("Skipping core.ping: every concurrent call place is taken");
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3384
3546
|
this.interceptor?.outgoing?.(ping);
|
|
3385
3547
|
ws.next(ping);
|
|
3386
3548
|
});
|
|
3549
|
+
if (this.limiter) {
|
|
3550
|
+
const limiter = this.limiter;
|
|
3551
|
+
this.socketMessages$.pipe(takeUntil(this.closeConnection)).subscribe((message) => {
|
|
3552
|
+
limiter.received(message);
|
|
3553
|
+
});
|
|
3554
|
+
let wasOpen = false;
|
|
3555
|
+
this.opened$.pipe(takeUntil(this.closeConnection)).subscribe((isOpen) => {
|
|
3556
|
+
if (wasOpen && !isOpen) {
|
|
3557
|
+
limiter.socketLost();
|
|
3558
|
+
}
|
|
3559
|
+
wasOpen = isOpen;
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3387
3562
|
if (interceptor?.incoming) {
|
|
3388
3563
|
const incoming = interceptor.incoming.bind(interceptor);
|
|
3389
3564
|
this.messages$.pipe(takeUntil(this.closeConnection)).subscribe((message) => incoming(message));
|
|
@@ -3454,11 +3629,46 @@ var TrueNasConnection = class {
|
|
|
3454
3629
|
* sends a message over the current websocket OR queues it to send once
|
|
3455
3630
|
* the next websocket is opened. messages sent via `send` will not be lost until after
|
|
3456
3631
|
* the next `closed$` emission.
|
|
3632
|
+
*
|
|
3633
|
+
* With `maxConcurrentCalls` set, a request beyond the limit waits for an
|
|
3634
|
+
* answer to make room. Unsubscribing the returned handle takes back a frame
|
|
3635
|
+
* not written yet, waiting or queued.
|
|
3457
3636
|
*/
|
|
3458
3637
|
send(message) {
|
|
3459
3638
|
if (this.interceptor?.outgoing?.(message) === false) {
|
|
3460
3639
|
return Subscription.EMPTY;
|
|
3461
3640
|
}
|
|
3641
|
+
return this.limiter ? this.limiter.send(message) : this.writeToNextSocket(message);
|
|
3642
|
+
}
|
|
3643
|
+
/**
|
|
3644
|
+
* Lets calls held from a lost socket go out again. The client calls this
|
|
3645
|
+
* whenever the session is authenticated, so a consumer does not need to;
|
|
3646
|
+
* a no-op without `maxConcurrentCalls`, or with nothing held.
|
|
3647
|
+
*
|
|
3648
|
+
* Calls waiting for a place when a socket is lost are held, not sent on the
|
|
3649
|
+
* next one: a new socket starts signed out, so middleware would refuse them
|
|
3650
|
+
* with `ENOTAUTHENTICATED`.
|
|
3651
|
+
*/
|
|
3652
|
+
releaseHeldCalls() {
|
|
3653
|
+
this.limiter?.release();
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Ends every call held from a lost socket without sending it, answering each
|
|
3657
|
+
* on `messages()` with the refusal middleware would have given it on a socket
|
|
3658
|
+
* that is not signed in: `ENOTAUTHENTICATED`. Returns how many it ended.
|
|
3659
|
+
*
|
|
3660
|
+
* For an app whose next sign-in may be a different user than the one who made
|
|
3661
|
+
* those calls, e.g. a password prompt after the session was lost. Call it
|
|
3662
|
+
* before that sign-in; otherwise the calls go out once it succeeds. A no-op
|
|
3663
|
+
* while nothing is held, so a healthy session's waiting calls are not touched.
|
|
3664
|
+
*/
|
|
3665
|
+
refuseHeldCalls() {
|
|
3666
|
+
const ids = this.limiter?.takeHeld() ?? [];
|
|
3667
|
+
ids.forEach((id) => this.refusals$.next(notAuthenticatedRefusal(id)));
|
|
3668
|
+
return ids.length;
|
|
3669
|
+
}
|
|
3670
|
+
/** Writes to the current socket, or to the next one once it opens. */
|
|
3671
|
+
writeToNextSocket(message) {
|
|
3462
3672
|
return this.ws$.pipe(
|
|
3463
3673
|
filter((ws) => ws !== null),
|
|
3464
3674
|
take(1)
|
|
@@ -3602,6 +3812,22 @@ var makeConnectionError = (message, hostname, closeCode, closeReason) => ({
|
|
|
3602
3812
|
var isTerminalClose = (err) => err?.closeCode === policyViolationCloseCode;
|
|
3603
3813
|
var resolveEndpoint = (hostnames, protocol) => Object.freeze({ hostnames: Object.freeze([...hostnames]), protocol });
|
|
3604
3814
|
var sameEndpoint = (a, b) => a.protocol === b.protocol && a.hostnames.length === b.hostnames.length && a.hostnames.every((hostname, i) => hostname === b.hostnames[i]);
|
|
3815
|
+
var notAuthenticatedRefusal = (id) => ({
|
|
3816
|
+
jsonrpc: "2.0",
|
|
3817
|
+
id,
|
|
3818
|
+
error: {
|
|
3819
|
+
code: -32001,
|
|
3820
|
+
message: "Method call error",
|
|
3821
|
+
data: {
|
|
3822
|
+
// `ErrnoMixin.ENOTAUTHENTICATED` in truenas_api_client.
|
|
3823
|
+
error: 207,
|
|
3824
|
+
errname: "ENOTAUTHENTICATED",
|
|
3825
|
+
reason: "The session this call was made in ended before it could be sent.",
|
|
3826
|
+
extra: null,
|
|
3827
|
+
trace: null
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
});
|
|
3605
3831
|
var closedConnection = { state: "closed" };
|
|
3606
3832
|
|
|
3607
3833
|
// src/client/truenas-api-client.ts
|
|
@@ -3619,6 +3845,11 @@ var TrueNasApiClient = class {
|
|
|
3619
3845
|
this.authenticator = this.createAuthenticator();
|
|
3620
3846
|
this.api = this.createApi();
|
|
3621
3847
|
this.ops = this.createOperations();
|
|
3848
|
+
this.authenticator.authenticated$.pipe(
|
|
3849
|
+
distinctUntilChanged(),
|
|
3850
|
+
filter(Boolean),
|
|
3851
|
+
takeUntil(this.connection.closeConnection)
|
|
3852
|
+
).subscribe(() => this.connection.releaseHeldCalls());
|
|
3622
3853
|
}
|
|
3623
3854
|
/**
|
|
3624
3855
|
* Get current connection status.
|
|
@@ -3657,7 +3888,8 @@ var TrueNasApiClient = class {
|
|
|
3657
3888
|
this.reconnect.maxRetry,
|
|
3658
3889
|
this.logger,
|
|
3659
3890
|
this.protocol,
|
|
3660
|
-
this.reconnect.interceptor
|
|
3891
|
+
this.reconnect.interceptor,
|
|
3892
|
+
this.reconnect.maxConcurrentCalls
|
|
3661
3893
|
);
|
|
3662
3894
|
}
|
|
3663
3895
|
/**
|
|
@@ -4522,7 +4754,12 @@ function instantiateClientForVersion(version, opts, logger) {
|
|
|
4522
4754
|
systemName,
|
|
4523
4755
|
logger,
|
|
4524
4756
|
opts.protocol,
|
|
4525
|
-
{
|
|
4757
|
+
{
|
|
4758
|
+
retryDelay: opts.retryDelay,
|
|
4759
|
+
maxRetry: opts.maxRetry,
|
|
4760
|
+
interceptor: opts.interceptor,
|
|
4761
|
+
maxConcurrentCalls: opts.maxConcurrentCalls
|
|
4762
|
+
}
|
|
4526
4763
|
);
|
|
4527
4764
|
}
|
|
4528
4765
|
function errorMessageOrDefault(error, fallback) {
|
|
@@ -4542,5 +4779,5 @@ var UserRole = /* @__PURE__ */ ((UserRole2) => {
|
|
|
4542
4779
|
})(UserRole || {});
|
|
4543
4780
|
|
|
4544
4781
|
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-
|
|
4546
|
-
//# sourceMappingURL=chunk-
|
|
4782
|
+
//# sourceMappingURL=chunk-R4VP3HBB.js.map
|
|
4783
|
+
//# sourceMappingURL=chunk-R4VP3HBB.js.map
|