@milaboratories/pl-client 3.14.5 → 3.14.7
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/dist/core/client.cjs +10 -2
- package/dist/core/client.cjs.map +1 -1
- package/dist/core/client.d.ts +4 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +10 -2
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.cjs +37 -1
- package/dist/core/config.cjs.map +1 -1
- package/dist/core/config.d.ts +25 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +31 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.cjs +7 -0
- package/dist/core/errors.cjs.map +1 -1
- package/dist/core/errors.d.ts +5 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +7 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/ll_client.cjs +64 -7
- package/dist/core/ll_client.cjs.map +1 -1
- package/dist/core/ll_client.d.ts +20 -0
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/ll_client.js +66 -9
- package/dist/core/ll_client.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/package.json +6 -6
- package/src/core/client.ts +9 -0
- package/src/core/config.test.ts +70 -1
- package/src/core/config.ts +43 -0
- package/src/core/error.test.ts +23 -1
- package/src/core/errors.ts +7 -0
- package/src/core/ll_client.ts +120 -28
package/src/core/config.ts
CHANGED
|
@@ -90,6 +90,11 @@ export interface PlClientConfig {
|
|
|
90
90
|
|
|
91
91
|
/** Value from 0 to 1, determine level of randomness to introduce to the backoff delays sequence. (0 meaning no randomness) */
|
|
92
92
|
retryJitter: number;
|
|
93
|
+
|
|
94
|
+
/** Upper bound for a single backoff delay, in ms. Without it the exponential
|
|
95
|
+
* sequence keeps growing across all attempts: at the defaults the last sleep
|
|
96
|
+
* alone reaches ~66s (~150s once jitter compounds). */
|
|
97
|
+
retryMaxDelay: number;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
export const DEFAULT_REQUEST_TIMEOUT = 5_000;
|
|
@@ -106,6 +111,41 @@ export const DEFAULT_RETRY_INITIAL_DELAY = 20; // 20 ms * <jitter> of sleep afte
|
|
|
106
111
|
export const DEFAULT_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER = 1.5; // + 50% on each round
|
|
107
112
|
export const DEFAULT_RETRY_LINEAR_BACKOFF_STEP = 50; // + 50 ms
|
|
108
113
|
export const DEFAULT_RETRY_JITTER = 0.3; // 30%
|
|
114
|
+
export const DEFAULT_RETRY_MAX_DELAY = 5_000; // 5 seconds
|
|
115
|
+
|
|
116
|
+
/** Multiplier applied to the observed RTT when deriving the unary deadline. A unary call
|
|
117
|
+
* costs at least one round trip, plus connect/DNS/LB and server work on top, so the
|
|
118
|
+
* deadline needs sizeable headroom over the bare RTT. */
|
|
119
|
+
export const RTT_DEADLINE_MULTIPLIER = 8;
|
|
120
|
+
|
|
121
|
+
/** Ceiling for the RTT-derived unary deadline. Beyond this a call is stuck rather than
|
|
122
|
+
* slow, and waiting longer only delays the retry. */
|
|
123
|
+
export const MAX_ADAPTIVE_REQUEST_TIMEOUT = 60_000;
|
|
124
|
+
|
|
125
|
+
/** Weight of the newest RTT sample in the smoothed estimate. Low enough that one unlucky
|
|
126
|
+
* sample cannot swing the deadline, high enough to follow a real change. */
|
|
127
|
+
export const RTT_SMOOTHING_ALPHA = 0.3;
|
|
128
|
+
|
|
129
|
+
/** Login runs password hashing (and possibly an external IdP round trip) server side, so
|
|
130
|
+
* it is legitimately slow and gets its own deadline instead of the unary default. */
|
|
131
|
+
export const DEFAULT_LOGIN_TIMEOUT = 120_000;
|
|
132
|
+
|
|
133
|
+
/** Unary deadline for a given RTT estimate: floored by the configured timeout so a fast
|
|
134
|
+
* link behaves exactly as before, capped by {@link MAX_ADAPTIVE_REQUEST_TIMEOUT}.
|
|
135
|
+
* An undefined `rttMs` (no ping yet) yields the configured value unchanged. */
|
|
136
|
+
export function deriveUnaryDeadline(configuredMs: number, rttMs: number | undefined): number {
|
|
137
|
+
if (rttMs === undefined) return configuredMs;
|
|
138
|
+
return Math.min(
|
|
139
|
+
MAX_ADAPTIVE_REQUEST_TIMEOUT,
|
|
140
|
+
Math.max(configuredMs, Math.ceil(rttMs * RTT_DEADLINE_MULTIPLIER)),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Folds a new round-trip sample into an exponentially smoothed estimate. */
|
|
145
|
+
export function smoothRtt(previousMs: number | undefined, sampleMs: number): number {
|
|
146
|
+
if (previousMs === undefined) return sampleMs;
|
|
147
|
+
return previousMs * (1 - RTT_SMOOTHING_ALPHA) + sampleMs * RTT_SMOOTHING_ALPHA;
|
|
148
|
+
}
|
|
109
149
|
|
|
110
150
|
export const DefaultRetryOptions: ExponentialBackoffRetryOptions = {
|
|
111
151
|
type: "exponentialBackoff",
|
|
@@ -113,6 +153,7 @@ export const DefaultRetryOptions: ExponentialBackoffRetryOptions = {
|
|
|
113
153
|
initialDelay: DEFAULT_RETRY_INITIAL_DELAY,
|
|
114
154
|
backoffMultiplier: DEFAULT_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER,
|
|
115
155
|
jitter: DEFAULT_RETRY_JITTER,
|
|
156
|
+
maxDelay: DEFAULT_RETRY_MAX_DELAY,
|
|
116
157
|
};
|
|
117
158
|
|
|
118
159
|
type PlConfigOverrides = Partial<
|
|
@@ -161,6 +202,7 @@ export function plAddressToConfig(
|
|
|
161
202
|
retryExponentialBackoffMultiplier: DEFAULT_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER,
|
|
162
203
|
retryLinearBackoffStep: DEFAULT_RETRY_LINEAR_BACKOFF_STEP,
|
|
163
204
|
retryJitter: DEFAULT_RETRY_JITTER,
|
|
205
|
+
retryMaxDelay: DEFAULT_RETRY_MAX_DELAY,
|
|
164
206
|
|
|
165
207
|
...overrides,
|
|
166
208
|
};
|
|
@@ -234,6 +276,7 @@ export function plAddressToConfig(
|
|
|
234
276
|
parseInt(url.searchParams.get("retry-linear-backoff-step")) ??
|
|
235
277
|
DEFAULT_RETRY_LINEAR_BACKOFF_STEP,
|
|
236
278
|
retryJitter: parseInt(url.searchParams.get("retry-backoff-jitter")) ?? DEFAULT_RETRY_JITTER,
|
|
279
|
+
retryMaxDelay: parseInt(url.searchParams.get("retry-max-delay")) ?? DEFAULT_RETRY_MAX_DELAY,
|
|
237
280
|
|
|
238
281
|
...overrides,
|
|
239
282
|
};
|
package/src/core/error.test.ts
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import * as tp from "node:timers/promises";
|
|
2
|
-
import { isTimeoutOrCancelError } from "./errors";
|
|
2
|
+
import { isTimeoutOrCancelError, isTransientCallFailure } from "./errors";
|
|
3
3
|
import { test, expect } from "vitest";
|
|
4
4
|
|
|
5
|
+
/** Shapes the predicates match on: an RpcError-like from grpc, a RESTError-like from REST. */
|
|
6
|
+
function rpcError(code: string) {
|
|
7
|
+
return { name: "RpcError", code };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test("only transport failures count as transient", () => {
|
|
11
|
+
// Worth retrying: the call never reached a verdict.
|
|
12
|
+
expect(isTransientCallFailure(rpcError("UNAVAILABLE"))).toBe(true);
|
|
13
|
+
expect(isTransientCallFailure(rpcError("DEADLINE_EXCEEDED"))).toBe(true);
|
|
14
|
+
|
|
15
|
+
// Real answers from the server. Retrying these would hide a genuine failure and,
|
|
16
|
+
// for auth, silently repeat a rejected credential.
|
|
17
|
+
expect(isTransientCallFailure(rpcError("UNAUTHENTICATED"))).toBe(false);
|
|
18
|
+
expect(isTransientCallFailure(rpcError("PERMISSION_DENIED"))).toBe(false);
|
|
19
|
+
expect(isTransientCallFailure(rpcError("NOT_FOUND"))).toBe(false);
|
|
20
|
+
expect(isTransientCallFailure(rpcError("UNIMPLEMENTED"))).toBe(false);
|
|
21
|
+
|
|
22
|
+
expect(isTransientCallFailure(undefined)).toBe(false);
|
|
23
|
+
expect(isTransientCallFailure(null)).toBe(false);
|
|
24
|
+
expect(isTransientCallFailure(new Error("plain"))).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
5
27
|
test("timeout of sleep error type detection", async () => {
|
|
6
28
|
let noError = false;
|
|
7
29
|
try {
|
package/src/core/errors.ts
CHANGED
|
@@ -82,6 +82,13 @@ export function isTimeoutOrCancelError(err: unknown, nested: boolean = false): b
|
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/** Failures worth another attempt on an idempotent call: the peer was unreachable, or the
|
|
86
|
+
* deadline elapsed. Everything else (auth, permission, not-found, unimplemented) is a real
|
|
87
|
+
* answer from the server, and retrying it only wastes the caller's time. */
|
|
88
|
+
export function isTransientCallFailure(err: unknown): boolean {
|
|
89
|
+
return isConnectionProblem(err) || isTimeoutError(err);
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
export function isUnimplementedError(err: unknown, nested: boolean = false): boolean {
|
|
86
93
|
if (err === undefined || err === null) return false;
|
|
87
94
|
|
package/src/core/ll_client.ts
CHANGED
|
@@ -13,7 +13,14 @@ import type {
|
|
|
13
13
|
PlConnectionStatus,
|
|
14
14
|
PlConnectionStatusListener,
|
|
15
15
|
} from "./config";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
plAddressToConfig,
|
|
18
|
+
type wireProtocol,
|
|
19
|
+
SUPPORTED_WIRE_PROTOCOLS,
|
|
20
|
+
DEFAULT_LOGIN_TIMEOUT,
|
|
21
|
+
deriveUnaryDeadline,
|
|
22
|
+
smoothRtt,
|
|
23
|
+
} from "./config";
|
|
17
24
|
import type { GrpcOptions } from "@protobuf-ts/grpc-transport";
|
|
18
25
|
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
|
|
19
26
|
import { LLPlTransaction } from "./ll_transaction";
|
|
@@ -42,7 +49,7 @@ import {
|
|
|
42
49
|
TxAPI_ServerMessage,
|
|
43
50
|
} from "../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api";
|
|
44
51
|
import type { MiLogger } from "@milaboratories/ts-helpers";
|
|
45
|
-
import { isAbortedError, isUnauthenticated } from "./errors";
|
|
52
|
+
import { isAbortedError, isTransientCallFailure, isUnauthenticated } from "./errors";
|
|
46
53
|
import { Timestamp } from "../proto-grpc/google/protobuf/timestamp";
|
|
47
54
|
|
|
48
55
|
export interface PlCallOps {
|
|
@@ -50,6 +57,17 @@ export interface PlCallOps {
|
|
|
50
57
|
abortSignal?: AbortSignal;
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
/** Bounded retry for idempotent unary calls. Deliberately short: these calls sit on the
|
|
61
|
+
* connect path and gate the UI, so a few quick attempts beat a long grind. */
|
|
62
|
+
const IDEMPOTENT_RETRY_OPTIONS: RetryOptions = {
|
|
63
|
+
type: "exponentialBackoff",
|
|
64
|
+
maxAttempts: 4,
|
|
65
|
+
initialDelay: 200,
|
|
66
|
+
backoffMultiplier: 2,
|
|
67
|
+
jitter: 0.3,
|
|
68
|
+
maxDelay: 2_000,
|
|
69
|
+
};
|
|
70
|
+
|
|
53
71
|
// Parses leading "<major>.<minor>.<patch>" from a version string like
|
|
54
72
|
// "3.1.1" or "3.1.1-rc1" and returns true if the parsed version is >= target.
|
|
55
73
|
// Returns false for unparseable versions (safer to assume an old backend).
|
|
@@ -128,6 +146,15 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
128
146
|
private _wireProto: wireProtocol = "grpc";
|
|
129
147
|
private _wireConn!: WireConnection;
|
|
130
148
|
|
|
149
|
+
/** The live GrpcOptions object handed to GrpcTransport. The transport keeps it by
|
|
150
|
+
* reference and re-reads it on every call, so mutating `timeout` here retunes
|
|
151
|
+
* subsequent deadlines without tearing down the connection. */
|
|
152
|
+
private _grpcOptions?: GrpcOptions;
|
|
153
|
+
|
|
154
|
+
/** Smoothed round-trip estimate in ms, from ping timings. Undefined until the first
|
|
155
|
+
* successful ping, in which case the configured deadline is used as-is. */
|
|
156
|
+
private _rttMs?: number;
|
|
157
|
+
|
|
131
158
|
private readonly _restInterceptors: Dispatcher.DispatcherComposeInterceptor[];
|
|
132
159
|
private readonly _restMiddlewares: Middleware[];
|
|
133
160
|
private readonly _grpcInterceptors: Interceptor[];
|
|
@@ -160,7 +187,10 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
160
187
|
// Guarantee a ping happened so capability-gated paths (login, refresh) can branch synchronously.
|
|
161
188
|
// In the autodetect path the loop's last successful ping already populated _serverInfo via the
|
|
162
189
|
// side-effect in ping(); this fallback covers the path where autodetect is disabled.
|
|
163
|
-
|
|
190
|
+
// Retried, not bare: this ping is the first contact with the server, so DNS and LB
|
|
191
|
+
// warm-up land here. A single transient failure must not fail the whole connect.
|
|
192
|
+
// (Not inside ping() itself: the autodetect path already wraps it in its own retry.)
|
|
193
|
+
if (!pl._serverInfo) await pl.withIdempotentRetry("ping", () => pl.ping());
|
|
164
194
|
|
|
165
195
|
// Install the process-global signature-strictness flag based on backend version.
|
|
166
196
|
setResourceSignaturesRequired(pl.supportsResourceSignatures);
|
|
@@ -284,7 +314,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
284
314
|
//
|
|
285
315
|
const grpcOptions: GrpcOptions = {
|
|
286
316
|
host: this.conf.hostAndPort,
|
|
287
|
-
timeout: this.
|
|
317
|
+
timeout: this.unaryDeadline(),
|
|
288
318
|
channelCredentials: this.conf.ssl
|
|
289
319
|
? ChannelCredentials.createSsl()
|
|
290
320
|
: ChannelCredentials.createInsecure(),
|
|
@@ -309,9 +339,48 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
309
339
|
delete process.env.grpc_proxy;
|
|
310
340
|
}
|
|
311
341
|
|
|
342
|
+
this._grpcOptions = grpcOptions;
|
|
312
343
|
this._replaceWireConnection({ type: "grpc", Transport: new GrpcTransport(grpcOptions) });
|
|
313
344
|
}
|
|
314
345
|
|
|
346
|
+
/** Unary deadline derived from the observed RTT, floored by the configured value so
|
|
347
|
+
* a fast link behaves exactly as before, and capped by
|
|
348
|
+
* {@link MAX_ADAPTIVE_REQUEST_TIMEOUT}. */
|
|
349
|
+
private unaryDeadline(): number {
|
|
350
|
+
return deriveUnaryDeadline(this.conf.defaultRequestTimeout, this._rttMs);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Folds a fresh round-trip sample into the estimate and retunes the live unary
|
|
354
|
+
* deadline. Called after every successful ping. */
|
|
355
|
+
private recordRtt(sampleMs: number): void {
|
|
356
|
+
this._rttMs = smoothRtt(this._rttMs, sampleMs);
|
|
357
|
+
|
|
358
|
+
if (this._grpcOptions) {
|
|
359
|
+
const next = this.unaryDeadline();
|
|
360
|
+
if (next !== this._grpcOptions.timeout) {
|
|
361
|
+
this.ops.logger?.info(
|
|
362
|
+
`Unary deadline retuned to ${next}ms (rtt estimate ${Math.round(this._rttMs)}ms)`,
|
|
363
|
+
);
|
|
364
|
+
this._grpcOptions.timeout = next;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Smoothed round-trip estimate in ms, or undefined before the first ping. */
|
|
370
|
+
public get rttEstimateMs(): number | undefined {
|
|
371
|
+
return this._rttMs;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Runs an idempotent unary call with a bounded retry on transient transport failures.
|
|
375
|
+
* Safe only for calls with no side effects, since a retry may duplicate the request. */
|
|
376
|
+
private async withIdempotentRetry<T>(name: string, cb: () => Promise<T>): Promise<T> {
|
|
377
|
+
return await retry(cb, IDEMPOTENT_RETRY_OPTIONS, (e: unknown) => {
|
|
378
|
+
if (!isTransientCallFailure(e)) return false;
|
|
379
|
+
this.ops.logger?.info(`${name}: transient failure, retrying. err=${String(e)}`);
|
|
380
|
+
return true;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
315
384
|
private _replaceWireConnection(newConn: WireConnection): void {
|
|
316
385
|
const oldConn = this._wireConn;
|
|
317
386
|
this._wireConn = newConn;
|
|
@@ -562,7 +631,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
562
631
|
expiration: { seconds: ttlSeconds, nanos: 0 },
|
|
563
632
|
requestedRole: role,
|
|
564
633
|
},
|
|
565
|
-
{ meta },
|
|
634
|
+
{ meta, timeout: DEFAULT_LOGIN_TIMEOUT },
|
|
566
635
|
).response
|
|
567
636
|
).token;
|
|
568
637
|
} else {
|
|
@@ -588,14 +657,17 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
588
657
|
|
|
589
658
|
if (cl instanceof GrpcPlApiClient) {
|
|
590
659
|
return (
|
|
591
|
-
await cl.login(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
660
|
+
await cl.login(
|
|
661
|
+
{
|
|
662
|
+
credentials: {
|
|
663
|
+
oneofKind: "basic",
|
|
664
|
+
basic: { login: user, password },
|
|
665
|
+
},
|
|
666
|
+
expiration: { seconds: ttl, nanos: 0 },
|
|
667
|
+
requestedRole: role,
|
|
595
668
|
},
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}).response
|
|
669
|
+
{ timeout: DEFAULT_LOGIN_TIMEOUT },
|
|
670
|
+
).response
|
|
599
671
|
).token;
|
|
600
672
|
} else {
|
|
601
673
|
const resp = cl.POST("/v1/auth/login", {
|
|
@@ -624,14 +696,17 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
624
696
|
|
|
625
697
|
if (cl instanceof GrpcPlApiClient) {
|
|
626
698
|
return (
|
|
627
|
-
await cl.login(
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
699
|
+
await cl.login(
|
|
700
|
+
{
|
|
701
|
+
credentials: {
|
|
702
|
+
oneofKind: "token",
|
|
703
|
+
token: { token: bytes },
|
|
704
|
+
},
|
|
705
|
+
expiration: { seconds: ttl, nanos: 0 },
|
|
706
|
+
requestedRole: role,
|
|
631
707
|
},
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}).response
|
|
708
|
+
{ timeout: DEFAULT_LOGIN_TIMEOUT },
|
|
709
|
+
).response
|
|
635
710
|
).token;
|
|
636
711
|
} else {
|
|
637
712
|
const resp = cl.POST("/v1/auth/login", {
|
|
@@ -653,12 +728,15 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
653
728
|
const cl = this.clientProvider.get();
|
|
654
729
|
if (cl instanceof GrpcPlApiClient) {
|
|
655
730
|
return (
|
|
656
|
-
await cl.login(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
731
|
+
await cl.login(
|
|
732
|
+
{
|
|
733
|
+
credentials: {
|
|
734
|
+
oneofKind: "sso",
|
|
735
|
+
sso: { tokenResponse },
|
|
736
|
+
},
|
|
660
737
|
},
|
|
661
|
-
|
|
738
|
+
{ timeout: DEFAULT_LOGIN_TIMEOUT },
|
|
739
|
+
).response
|
|
662
740
|
).token;
|
|
663
741
|
} else {
|
|
664
742
|
const resp = cl.POST("/v1/auth/login", {
|
|
@@ -711,10 +789,13 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
711
789
|
|
|
712
790
|
if (cl instanceof GrpcPlApiClient) {
|
|
713
791
|
return (
|
|
714
|
-
await cl.refreshToken(
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
792
|
+
await cl.refreshToken(
|
|
793
|
+
{
|
|
794
|
+
token: currentToken,
|
|
795
|
+
expiration: { seconds: ttl, nanos: 0 },
|
|
796
|
+
},
|
|
797
|
+
{ timeout: DEFAULT_LOGIN_TIMEOUT },
|
|
798
|
+
).response
|
|
718
799
|
).token;
|
|
719
800
|
} else {
|
|
720
801
|
const resp = cl.POST("/v1/auth/refresh", {
|
|
@@ -727,6 +808,8 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
727
808
|
public async ping(): Promise<grpcTypes.MaintenanceAPI_Ping_Response> {
|
|
728
809
|
const cl = this.clientProvider.get();
|
|
729
810
|
let resp: grpcTypes.MaintenanceAPI_Ping_Response;
|
|
811
|
+
// Ping is the cheapest call we make, so its duration is our best RTT proxy.
|
|
812
|
+
const startedAt = performance.now();
|
|
730
813
|
if (cl instanceof GrpcPlApiClient) {
|
|
731
814
|
resp = (await cl.ping({})).response;
|
|
732
815
|
} else {
|
|
@@ -741,6 +824,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
741
824
|
capabilities: (pingData as any).capabilities ?? [],
|
|
742
825
|
};
|
|
743
826
|
}
|
|
827
|
+
this.recordRtt(performance.now() - startedAt);
|
|
744
828
|
this._serverInfo = resp;
|
|
745
829
|
return resp;
|
|
746
830
|
}
|
|
@@ -899,6 +983,14 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
899
983
|
|
|
900
984
|
public async getUserRoot(
|
|
901
985
|
opts: { login?: string; createIfNotExists?: boolean } = {},
|
|
986
|
+
): Promise<grpcTypes.AuthAPI_GetUserRoot_Response> {
|
|
987
|
+
// Retryable even with createIfNotExists: the call is get-or-create keyed on login, so a
|
|
988
|
+
// second attempt returns the existing root rather than making another one.
|
|
989
|
+
return await this.withIdempotentRetry("getUserRoot", () => this.getUserRootOnce(opts));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private async getUserRootOnce(
|
|
993
|
+
opts: { login?: string; createIfNotExists?: boolean } = {},
|
|
902
994
|
): Promise<grpcTypes.AuthAPI_GetUserRoot_Response> {
|
|
903
995
|
const cl = this.clientProvider.get();
|
|
904
996
|
if (cl instanceof GrpcPlApiClient) {
|