@milaboratories/pl-client 2.16.13 → 2.16.15
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/__external/.pnpm/{@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3 → @rollup_plugin-typescript@12.3.0_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3}/__external/tslib/tslib.es6.cjs.map +1 -1
- package/dist/__external/.pnpm/{@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3 → @rollup_plugin-typescript@12.3.0_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3}/__external/tslib/tslib.es6.js.map +1 -1
- package/dist/core/client.cjs +31 -16
- package/dist/core/client.cjs.map +1 -1
- package/dist/core/client.d.ts +3 -2
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +31 -16
- package/dist/core/client.js.map +1 -1
- package/dist/core/default_client.cjs +1 -1
- package/dist/core/default_client.cjs.map +1 -1
- package/dist/core/default_client.js +1 -1
- package/dist/core/default_client.js.map +1 -1
- package/dist/core/errors.cjs +71 -26
- package/dist/core/errors.cjs.map +1 -1
- package/dist/core/errors.d.ts +10 -3
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +68 -27
- package/dist/core/errors.js.map +1 -1
- package/dist/core/ll_client.cjs +48 -18
- package/dist/core/ll_client.cjs.map +1 -1
- package/dist/core/ll_client.d.ts +12 -3
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/ll_client.js +49 -19
- package/dist/core/ll_client.js.map +1 -1
- package/dist/core/transaction.cjs +1 -1
- package/dist/core/transaction.js +1 -1
- package/dist/core/unauth_client.cjs +6 -2
- package/dist/core/unauth_client.cjs.map +1 -1
- package/dist/core/unauth_client.d.ts +2 -1
- package/dist/core/unauth_client.d.ts.map +1 -1
- package/dist/core/unauth_client.js +6 -2
- package/dist/core/unauth_client.js.map +1 -1
- package/dist/core/websocket_stream.cjs +23 -2
- package/dist/core/websocket_stream.cjs.map +1 -1
- package/dist/core/websocket_stream.d.ts.map +1 -1
- package/dist/core/websocket_stream.js +23 -2
- package/dist/core/websocket_stream.js.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/proto-rest/index.cjs +1 -1
- package/dist/proto-rest/index.cjs.map +1 -1
- package/dist/proto-rest/index.js +2 -2
- package/dist/proto-rest/index.js.map +1 -1
- package/dist/test/test_config.cjs +13 -3
- package/dist/test/test_config.cjs.map +1 -1
- package/dist/test/test_config.d.ts +4 -0
- package/dist/test/test_config.d.ts.map +1 -1
- package/dist/test/test_config.js +12 -4
- package/dist/test/test_config.js.map +1 -1
- package/package.json +10 -8
- package/src/core/client.ts +40 -21
- package/src/core/default_client.ts +1 -1
- package/src/core/errors.ts +61 -34
- package/src/core/ll_client.test.ts +18 -3
- package/src/core/ll_client.ts +63 -22
- package/src/core/unauth_client.test.ts +4 -4
- package/src/core/unauth_client.ts +7 -2
- package/src/core/websocket_stream.ts +22 -1
- package/src/proto-rest/index.ts +2 -2
- package/src/test/test_config.ts +13 -4
- /package/dist/__external/.pnpm/{@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3 → @rollup_plugin-typescript@12.3.0_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3}/__external/tslib/tslib.es6.cjs +0 -0
- /package/dist/__external/.pnpm/{@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3 → @rollup_plugin-typescript@12.3.0_rollup@4.52.4_tslib@2.8.1_typescript@5.6.3}/__external/tslib/tslib.es6.js +0 -0
package/src/core/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthOps, PlClientConfig, PlConnectionStatusListener } from './config';
|
|
1
|
+
import type { AuthOps, PlClientConfig, PlConnectionStatusListener, wireProtocol } from './config';
|
|
2
2
|
import type { PlCallOps } from './ll_client';
|
|
3
3
|
import { LLPlClient } from './ll_client';
|
|
4
4
|
import type { AnyResourceRef } from './transaction';
|
|
@@ -22,6 +22,7 @@ import type { AllTxStat, TxStat } from './stat';
|
|
|
22
22
|
import { addStat, initialTxStat } from './stat';
|
|
23
23
|
import type { WireConnection } from './wire';
|
|
24
24
|
import { advisoryLock } from './advisory_locks';
|
|
25
|
+
import { plAddressToConfig } from './config';
|
|
25
26
|
|
|
26
27
|
export type TxOps = PlCallOps & {
|
|
27
28
|
sync?: boolean;
|
|
@@ -54,8 +55,16 @@ export class PlClient {
|
|
|
54
55
|
/** Last resort measure to solve complicated race conditions in pl. */
|
|
55
56
|
private readonly defaultRetryOptions: RetryOptions;
|
|
56
57
|
|
|
57
|
-
private readonly buildLLPlClient: (shouldUseGzip: boolean) => LLPlClient
|
|
58
|
-
private _ll
|
|
58
|
+
private readonly buildLLPlClient: (shouldUseGzip: boolean, wireProtocol?: wireProtocol) => Promise<LLPlClient>;
|
|
59
|
+
private _ll?: LLPlClient;
|
|
60
|
+
|
|
61
|
+
private get ll(): LLPlClient {
|
|
62
|
+
if (this._ll === undefined) {
|
|
63
|
+
throw new Error('LLPlClient not initialized');
|
|
64
|
+
}
|
|
65
|
+
return this._ll;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
/** Stores client root (this abstraction is intended for future implementation of the security model) */
|
|
60
69
|
private _clientRoot: OptionalResourceId = NullResourceId;
|
|
61
70
|
|
|
@@ -83,12 +92,13 @@ export class PlClient {
|
|
|
83
92
|
finalPredicate?: FinalResourceDataPredicate;
|
|
84
93
|
} = {},
|
|
85
94
|
) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
const conf = typeof configOrAddress === 'string' ? plAddressToConfig(configOrAddress) : configOrAddress;
|
|
96
|
+
|
|
97
|
+
this.buildLLPlClient = async (shouldUseGzip: boolean, wireProtocol?: wireProtocol): Promise<LLPlClient> => {
|
|
98
|
+
if (wireProtocol) conf.wireProtocol = wireProtocol;
|
|
99
|
+
return await LLPlClient.build(conf, { auth, ...ops, shouldUseGzip });
|
|
89
100
|
};
|
|
90
|
-
|
|
91
|
-
const conf = this._ll.conf;
|
|
101
|
+
|
|
92
102
|
this.txDelay = conf.txDelay;
|
|
93
103
|
this.forceSync = conf.forceSync;
|
|
94
104
|
this.finalPredicate = ops.finalPredicate ?? DefaultFinalResourceDataPredicate;
|
|
@@ -145,23 +155,23 @@ export class PlClient {
|
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
public async ping(): Promise<MaintenanceAPI_Ping_Response> {
|
|
148
|
-
return await this.
|
|
158
|
+
return await this.ll.ping();
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
public async license(): Promise<MaintenanceAPI_License_Response> {
|
|
152
|
-
return await this.
|
|
162
|
+
return await this.ll.license();
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
public get conf(): PlClientConfig {
|
|
156
|
-
return this.
|
|
166
|
+
return this.ll.conf;
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
public get httpDispatcher(): Dispatcher {
|
|
160
|
-
return this.
|
|
170
|
+
return this.ll.httpDispatcher;
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
public get connectionOpts(): WireConnection {
|
|
164
|
-
return this.
|
|
174
|
+
return this.ll.wireConnection;
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
private get initialized() {
|
|
@@ -183,18 +193,27 @@ export class PlClient {
|
|
|
183
193
|
}
|
|
184
194
|
|
|
185
195
|
/** Currently implements custom logic to emulate future behaviour with single root. */
|
|
186
|
-
|
|
196
|
+
private async init() {
|
|
187
197
|
if (this.initialized) throw new Error('Already initialized');
|
|
188
198
|
|
|
199
|
+
// Initial client is created without gzip to perform server ping and detect optimal wire protocol.
|
|
200
|
+
// LLPlClient.build() internally calls detectOptimalWireProtocol() which starts with default 'grpc',
|
|
201
|
+
// then retries with 'rest' if ping fails, alternating until a working protocol is found.
|
|
202
|
+
// We save the detected wireProtocol here because if the server supports gzip compression,
|
|
203
|
+
// we'll need to reinitialize the client with gzip enabled - passing the already-detected
|
|
204
|
+
// wireProtocol avoids redundant protocol detection on reinit.
|
|
205
|
+
this._ll = await this.buildLLPlClient(false);
|
|
206
|
+
const wireProtocol = this._ll.wireProtocol;
|
|
207
|
+
|
|
189
208
|
// calculating reproducible root name from the username
|
|
190
209
|
const user = this._ll.authUser;
|
|
191
210
|
const mainRootName
|
|
192
|
-
|
|
211
|
+
= user === null ? AnonymousClientRoot : createHash('sha256').update(user).digest('hex');
|
|
193
212
|
|
|
194
213
|
this._serverInfo = await this.ping();
|
|
195
214
|
if (this._serverInfo.compression === MaintenanceAPI_Ping_Response_Compression.GZIP) {
|
|
196
215
|
await this._ll.close();
|
|
197
|
-
this._ll = this.buildLLPlClient(true);
|
|
216
|
+
this._ll = await this.buildLLPlClient(true, wireProtocol);
|
|
198
217
|
}
|
|
199
218
|
|
|
200
219
|
this._clientRoot = await this._withTx('initialization', true, NullResourceId, async (tx) => {
|
|
@@ -230,7 +249,7 @@ export class PlClient {
|
|
|
230
249
|
/** Returns true if field existed */
|
|
231
250
|
public async deleteAlternativeRoot(alternativeRootName: string): Promise<boolean> {
|
|
232
251
|
this.checkInitialized();
|
|
233
|
-
if (this.
|
|
252
|
+
if (this.ll.conf.alternativeRoot !== undefined)
|
|
234
253
|
throw new Error('Initialized with alternative root.');
|
|
235
254
|
return await this.withWriteTx('delete-alternative-root', async (tx) => {
|
|
236
255
|
const fId = {
|
|
@@ -259,7 +278,7 @@ export class PlClient {
|
|
|
259
278
|
|
|
260
279
|
try {
|
|
261
280
|
// opening low-level tx
|
|
262
|
-
const llTx = this.
|
|
281
|
+
const llTx = this.ll.createTx(writable, ops);
|
|
263
282
|
// wrapping it into high-level tx (this also asynchronously sends initialization message)
|
|
264
283
|
const tx = new PlTransaction(
|
|
265
284
|
llTx,
|
|
@@ -306,7 +325,7 @@ export class PlClient {
|
|
|
306
325
|
if (ok) {
|
|
307
326
|
// syncing on transaction if requested
|
|
308
327
|
if (ops?.sync === undefined ? this.forceSync : ops?.sync)
|
|
309
|
-
await this.
|
|
328
|
+
await this.ll.txSync(txId);
|
|
310
329
|
|
|
311
330
|
// introducing artificial delay, if requested
|
|
312
331
|
if (writable && this.txDelay > 0)
|
|
@@ -355,14 +374,14 @@ export class PlClient {
|
|
|
355
374
|
public getDriver<Drv extends PlDriver>(definition: PlDriverDefinition<Drv>): Drv {
|
|
356
375
|
const attached = this.drivers.get(definition.name);
|
|
357
376
|
if (attached !== undefined) return attached as Drv;
|
|
358
|
-
const driver = definition.init(this, this.
|
|
377
|
+
const driver = definition.init(this, this.ll, this.httpDispatcher);
|
|
359
378
|
this.drivers.set(definition.name, driver);
|
|
360
379
|
return driver;
|
|
361
380
|
}
|
|
362
381
|
|
|
363
382
|
/** Closes underlying transport */
|
|
364
383
|
public async close() {
|
|
365
|
-
await this.
|
|
384
|
+
await this.ll.close();
|
|
366
385
|
}
|
|
367
386
|
|
|
368
387
|
public static async init(
|
|
@@ -130,7 +130,7 @@ export async function defaultPlClient(): Promise<PlClient> {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (authInformation === undefined) {
|
|
133
|
-
const client =
|
|
133
|
+
const client = await UnauthenticatedPlClient.build(config);
|
|
134
134
|
|
|
135
135
|
if (await client.requireAuth()) {
|
|
136
136
|
if (config.user === undefined || config.password === undefined)
|
package/src/core/errors.ts
CHANGED
|
@@ -5,47 +5,65 @@ import { Code } from '../proto-grpc/google/rpc/code';
|
|
|
5
5
|
export function isConnectionProblem(err: unknown, nested: boolean = false): boolean {
|
|
6
6
|
if (err instanceof DisconnectedError) return true;
|
|
7
7
|
if ((err as any).name == 'RpcError' && (err as any).code == 'UNAVAILABLE') return true;
|
|
8
|
-
if ((err as any).code == Code.UNAVAILABLE) return true;
|
|
9
|
-
if ((err as any).cause !== undefined && !nested)
|
|
10
|
-
// nested limits the depth of search
|
|
11
|
-
return isConnectionProblem((err as any).cause, true);
|
|
8
|
+
if ((err as any).name == 'RESTError' && (err as any).status.code == Code.UNAVAILABLE) return true;
|
|
9
|
+
if ((err as any).cause !== undefined && !nested) return isConnectionProblem((err as any).cause, true);
|
|
12
10
|
return false;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
export function isUnauthenticated(err: unknown, nested: boolean = false): boolean {
|
|
16
14
|
if (err instanceof UnauthenticatedError) return true;
|
|
17
15
|
if ((err as any).name == 'RpcError' && (err as any).code == 'UNAUTHENTICATED') return true;
|
|
18
|
-
if ((err as any).code == Code.UNAUTHENTICATED) return true;
|
|
19
|
-
if ((err as any).cause !== undefined && !nested)
|
|
20
|
-
// nested limits the depth of search
|
|
21
|
-
return isUnauthenticated((err as any).cause, true);
|
|
16
|
+
if ((err as any).name == 'RESTError' && (err as any).status.code == Code.UNAUTHENTICATED) return true;
|
|
17
|
+
if ((err as any).cause !== undefined && !nested) return isUnauthenticated((err as any).cause, true);
|
|
22
18
|
return false;
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
export function
|
|
26
|
-
if (err instanceof Aborted || (err as any).name == 'AbortError') return true;
|
|
21
|
+
export function isTimeoutError(err: unknown, nested: boolean = false): boolean {
|
|
27
22
|
if ((err as any).name == 'TimeoutError') return true;
|
|
23
|
+
if ((err as any).name == 'RpcError' && (err as any).code == 'DEADLINE_EXCEEDED') return true;
|
|
24
|
+
if ((err as any).name == 'RESTError' && (err as any).status.code == Code.DEADLINE_EXCEEDED) return true;
|
|
25
|
+
if ((err as any).cause !== undefined && !nested) return isTimeoutError((err as any).cause, true);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isCancelError(err: unknown, nested: boolean = false): boolean {
|
|
30
|
+
if ((err as any).name == 'RpcError' && (err as any).code == 'CANCELLED') return true;
|
|
31
|
+
if ((err as any).name == 'RESTError' && (err as any).status.code == Code.CANCELLED) return true;
|
|
32
|
+
if ((err as any).cause !== undefined && !nested) return isCancelError((err as any).cause, true);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isAbortedError(err: unknown, nested: boolean = false): boolean {
|
|
37
|
+
if (err instanceof Aborted || (err as any).name == 'AbortError') return true;
|
|
28
38
|
if ((err as any).code == 'ABORT_ERR') return true;
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
return true;
|
|
35
|
-
if ((err as any).code == Code.CANCELLED || (err as any).code == Code.DEADLINE_EXCEEDED)
|
|
36
|
-
return true;
|
|
37
|
-
if ((err as any).cause !== undefined && !nested)
|
|
38
|
-
// nested limits the depth of search
|
|
39
|
-
return isTimeoutOrCancelError((err as any).cause, true);
|
|
39
|
+
if (err instanceof DOMException && err.code === DOMException.ABORT_ERR) return true; // WebSocket error
|
|
40
|
+
if ((err as any).name == 'RpcError' && (err as any).code == 'ABORTED') return true;
|
|
41
|
+
if ((err as any).name == 'RESTError' && (err as any).status.code == Code.ABORTED) return true;
|
|
42
|
+
if ((err as any).cause !== undefined && !nested) isAbortedError((err as any).cause, true);
|
|
40
43
|
return false;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
export
|
|
46
|
+
export function isTimeoutOrCancelError(err: unknown, nested: boolean = false): boolean {
|
|
47
|
+
if (isAbortedError(err, true)) return true;
|
|
48
|
+
if (isTimeoutError(err, true)) return true;
|
|
49
|
+
if (isCancelError(err, true)) return true;
|
|
50
|
+
if ((err as any).cause !== undefined && !nested) return isTimeoutOrCancelError((err as any).cause, true);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isNotFoundError(err: unknown, nested: boolean = false): boolean {
|
|
55
|
+
if ((err as any).name == 'RpcError' && (err as any).code == 'NOT_FOUND') return true;
|
|
56
|
+
if ((err as any).name == 'RESTError' && (err as any).status.code == Code.NOT_FOUND) return true;
|
|
57
|
+
if ((err as any).cause !== undefined && !nested) return isNotFoundError((err as any).cause, true);
|
|
58
|
+
return err instanceof RecoverablePlError && err.status.code === PlErrorCodeNotFound;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const PlErrorCodeNotFound: number = Code.NOT_FOUND;
|
|
44
62
|
|
|
45
63
|
export class PlError extends Error {
|
|
46
64
|
name = 'PlError';
|
|
47
|
-
constructor(public readonly status: Status) {
|
|
48
|
-
super(`code=${status.code} ${status.message}
|
|
65
|
+
constructor(public readonly status: Status, opts?: ErrorOptions) {
|
|
66
|
+
super(`code=${status.code} ${status.message}`, opts);
|
|
49
67
|
}
|
|
50
68
|
}
|
|
51
69
|
|
|
@@ -67,12 +85,6 @@ export class UnrecoverablePlError extends PlError {
|
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
export function isNotFoundError(err: unknown, nested: boolean = false): boolean {
|
|
71
|
-
if ((err as any).name == 'RpcError' && (err as any).code == 'NOT_FOUND') return true;
|
|
72
|
-
if ((err as any).cause !== undefined && !nested) return isNotFoundError((err as any).cause, true);
|
|
73
|
-
return err instanceof RecoverablePlError && err.status.code === PlErrorCodeNotFound;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
88
|
export class UnauthenticatedError extends Error {
|
|
77
89
|
name = 'UnauthenticatedError';
|
|
78
90
|
constructor(message: string) {
|
|
@@ -87,10 +99,25 @@ export class DisconnectedError extends Error {
|
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
export class RESTError extends PlError {
|
|
103
|
+
name = 'RESTError';
|
|
104
|
+
constructor(status: Status, opts?: ErrorOptions) {
|
|
105
|
+
super(status, opts);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
export function rethrowMeaningfulError(error: any, wrapIfUnknown: boolean = false): never {
|
|
91
|
-
if (isUnauthenticated(error))
|
|
92
|
-
|
|
110
|
+
if (isUnauthenticated(error)) {
|
|
111
|
+
if (error instanceof UnauthenticatedError) throw error;
|
|
112
|
+
throw new UnauthenticatedError(error.message);
|
|
113
|
+
}
|
|
114
|
+
if (isConnectionProblem(error)) {
|
|
115
|
+
if (error instanceof DisconnectedError) throw error;
|
|
116
|
+
throw new DisconnectedError(error.message);
|
|
117
|
+
}
|
|
93
118
|
if (isTimeoutOrCancelError(error)) throw new Aborted(error);
|
|
94
|
-
if (wrapIfUnknown)
|
|
95
|
-
|
|
119
|
+
if (wrapIfUnknown) {
|
|
120
|
+
const message = error.message || String(error) || 'Unknown error';
|
|
121
|
+
throw new Error(message, { cause: error });
|
|
122
|
+
} else throw error;
|
|
96
123
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LLPlClient } from './ll_client';
|
|
2
|
-
import { getTestConfig, getTestLLClient, getTestClientConf } from '../test/test_config';
|
|
2
|
+
import { getTestConfig, plAddressToTestConfig, getTestLLClient, getTestClientConf } from '../test/test_config';
|
|
3
3
|
import { TxAPI_Open_Request_WritableTx } from '../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api';
|
|
4
4
|
import { request } from 'undici';
|
|
5
5
|
import * as tp from 'node:timers/promises';
|
|
@@ -7,6 +7,15 @@ import { test, expect } from 'vitest';
|
|
|
7
7
|
|
|
8
8
|
import { UnauthenticatedError } from './errors';
|
|
9
9
|
|
|
10
|
+
test('wire protocol detection', async () => {
|
|
11
|
+
const { conf, auth } = await getTestClientConf();
|
|
12
|
+
const expectedWireProtocol = conf.wireProtocol ?? 'grpc';
|
|
13
|
+
conf.wireProtocol = undefined;
|
|
14
|
+
|
|
15
|
+
const client = await LLPlClient.build(conf, { auth });
|
|
16
|
+
expect(client.wireProtocol).toBe(expectedWireProtocol);
|
|
17
|
+
});
|
|
18
|
+
|
|
10
19
|
test('authenticated instance test', async () => {
|
|
11
20
|
const client = await getTestLLClient();
|
|
12
21
|
const tx = client.createTx(true);
|
|
@@ -29,7 +38,7 @@ test('unauthenticated status change', async () => {
|
|
|
29
38
|
return;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
const client =
|
|
41
|
+
const client = await LLPlClient.build(plAddressToTestConfig(cfg.address));
|
|
33
42
|
expect(client.status).toBe('OK');
|
|
34
43
|
|
|
35
44
|
const tx = client.createTx(true);
|
|
@@ -54,10 +63,16 @@ test('unauthenticated status change', async () => {
|
|
|
54
63
|
});
|
|
55
64
|
|
|
56
65
|
test('automatic token update', async () => {
|
|
66
|
+
const cfg = getTestConfig();
|
|
67
|
+
if (cfg.test_password === undefined) {
|
|
68
|
+
console.log("skipping test because target server doesn't support authentication");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
const { conf, auth } = await getTestClientConf();
|
|
58
73
|
conf.authMaxRefreshSeconds = 1;
|
|
59
74
|
let numberOfAuthUpdates = 0;
|
|
60
|
-
const client =
|
|
75
|
+
const client = await LLPlClient.build(conf, {
|
|
61
76
|
auth: {
|
|
62
77
|
authInformation: auth.authInformation,
|
|
63
78
|
onUpdate: (auth) => {
|
package/src/core/ll_client.ts
CHANGED
|
@@ -26,7 +26,7 @@ import type { WireClientProvider, WireClientProviderFactory, WireConnection } fr
|
|
|
26
26
|
import { parseHttpAuth } from '@milaboratories/pl-model-common';
|
|
27
27
|
import type * as grpcTypes from '../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api';
|
|
28
28
|
import { type PlApiPaths, type PlRestClientType, createClient, parseResponseError } from '../proto-rest';
|
|
29
|
-
import { notEmpty } from '@milaboratories/ts-helpers';
|
|
29
|
+
import { notEmpty, retry, withTimeout, type RetryOptions } from '@milaboratories/ts-helpers';
|
|
30
30
|
import { Code } from '../proto-grpc/google/rpc/code';
|
|
31
31
|
import { WebSocketBiDiStream } from './websocket_stream';
|
|
32
32
|
import { TxAPI_ClientMessage, TxAPI_ServerMessage } from '../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api';
|
|
@@ -54,8 +54,6 @@ class WireClientProviderImpl<Client> implements WireClientProvider<Client> {
|
|
|
54
54
|
|
|
55
55
|
/** Abstract out low level networking and authorization details */
|
|
56
56
|
export class LLPlClient implements WireClientProviderFactory {
|
|
57
|
-
public readonly conf: PlClientConfig;
|
|
58
|
-
|
|
59
57
|
/** Initial authorization information */
|
|
60
58
|
private authInformation?: AuthInformation;
|
|
61
59
|
/** Will be executed by the client when it is required */
|
|
@@ -70,7 +68,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
70
68
|
private _status: PlConnectionStatus = 'OK';
|
|
71
69
|
private readonly statusListener?: PlConnectionStatusListener;
|
|
72
70
|
|
|
73
|
-
private _wireProto: wireProtocol
|
|
71
|
+
private _wireProto: wireProtocol = 'grpc';
|
|
74
72
|
private _wireConn!: WireConnection;
|
|
75
73
|
|
|
76
74
|
private readonly _restInterceptors: Dispatcher.DispatcherComposeInterceptor[];
|
|
@@ -82,18 +80,29 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
82
80
|
|
|
83
81
|
public readonly httpDispatcher: Dispatcher;
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
public static async build(
|
|
86
84
|
configOrAddress: PlClientConfig | string,
|
|
87
|
-
|
|
85
|
+
ops: {
|
|
88
86
|
auth?: AuthOps;
|
|
89
87
|
statusListener?: PlConnectionStatusListener;
|
|
90
88
|
shouldUseGzip?: boolean;
|
|
91
89
|
} = {},
|
|
92
90
|
) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
const conf = typeof configOrAddress === 'string' ? plAddressToConfig(configOrAddress) : configOrAddress;
|
|
92
|
+
|
|
93
|
+
const pl = new LLPlClient(conf, ops);
|
|
94
|
+
await pl.detectOptimalWireProtocol();
|
|
95
|
+
return pl;
|
|
96
|
+
}
|
|
96
97
|
|
|
98
|
+
private constructor(
|
|
99
|
+
public readonly conf: PlClientConfig,
|
|
100
|
+
private readonly ops: {
|
|
101
|
+
auth?: AuthOps;
|
|
102
|
+
statusListener?: PlConnectionStatusListener;
|
|
103
|
+
shouldUseGzip?: boolean;
|
|
104
|
+
} = {},
|
|
105
|
+
) {
|
|
97
106
|
const { auth, statusListener } = ops;
|
|
98
107
|
|
|
99
108
|
if (auth !== undefined) {
|
|
@@ -120,8 +129,11 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
120
129
|
this._grpcInterceptors.push(this.createGrpcErrorInterceptor());
|
|
121
130
|
|
|
122
131
|
this.httpDispatcher = defaultHttpDispatcher(this.conf.httpProxy);
|
|
132
|
+
if (this.conf.wireProtocol) {
|
|
133
|
+
this._wireProto = this.conf.wireProtocol;
|
|
134
|
+
}
|
|
123
135
|
|
|
124
|
-
this.initWireConnection();
|
|
136
|
+
this.initWireConnection(this._wireProto);
|
|
125
137
|
|
|
126
138
|
if (statusListener !== undefined) {
|
|
127
139
|
this.statusListener = statusListener;
|
|
@@ -142,13 +154,8 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
142
154
|
});
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
private initWireConnection() {
|
|
146
|
-
|
|
147
|
-
// TODO: implement automatic server mode detection
|
|
148
|
-
this._wireProto = this.conf.wireProtocol ?? 'grpc';
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
switch (this._wireProto) {
|
|
157
|
+
private initWireConnection(protocol: wireProtocol) {
|
|
158
|
+
switch (protocol) {
|
|
152
159
|
case 'rest':
|
|
153
160
|
this.initRestConnection();
|
|
154
161
|
return;
|
|
@@ -158,7 +165,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
158
165
|
default:
|
|
159
166
|
((v: never) => {
|
|
160
167
|
throw new Error(`Unsupported wire protocol '${v as string}'. Use one of: ${SUPPORTED_WIRE_PROTOCOLS.join(', ')}`);
|
|
161
|
-
})(
|
|
168
|
+
})(protocol);
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
|
|
@@ -221,6 +228,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
221
228
|
private _replaceWireConnection(newConn: WireConnection): void {
|
|
222
229
|
const oldConn = this._wireConn;
|
|
223
230
|
this._wireConn = newConn;
|
|
231
|
+
this._wireProto = newConn.type;
|
|
224
232
|
|
|
225
233
|
// Reset all providers to let them reinitialize their clients
|
|
226
234
|
for (let i = 0; i < this.providers.length; i++) {
|
|
@@ -269,6 +277,10 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
269
277
|
return this._wireConn;
|
|
270
278
|
}
|
|
271
279
|
|
|
280
|
+
public get wireProtocol(): wireProtocol | undefined {
|
|
281
|
+
return this._wireProto;
|
|
282
|
+
}
|
|
283
|
+
|
|
272
284
|
/** Returns true if client is authenticated. Even with anonymous auth information
|
|
273
285
|
* connection is considered authenticated. Unauthenticated clients are used for
|
|
274
286
|
* login and similar tasks, see {@link UnauthenticatedPlClient}. */
|
|
@@ -431,7 +443,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
431
443
|
body: { expiration: `${ttlSeconds}s` },
|
|
432
444
|
headers,
|
|
433
445
|
});
|
|
434
|
-
return notEmpty((await resp).data).token;
|
|
446
|
+
return notEmpty((await resp).data, 'REST: empty response for JWT token request').token;
|
|
435
447
|
}
|
|
436
448
|
}
|
|
437
449
|
|
|
@@ -440,8 +452,37 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
440
452
|
if (cl instanceof GrpcPlApiClient) {
|
|
441
453
|
return (await cl.ping({})).response;
|
|
442
454
|
} else {
|
|
443
|
-
return notEmpty((await cl.GET('/v1/ping')).data);
|
|
455
|
+
return notEmpty((await cl.GET('/v1/ping')).data, 'REST: empty response for ping request');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Detects the best available wire protocol.
|
|
461
|
+
* If wireProtocol is explicitly configured, does nothing.
|
|
462
|
+
* Otherwise probes the current protocol via ping; if it fails, switches to the alternative.
|
|
463
|
+
*/
|
|
464
|
+
private async detectOptimalWireProtocol() {
|
|
465
|
+
if (this.conf.wireProtocol) {
|
|
466
|
+
return;
|
|
444
467
|
}
|
|
468
|
+
|
|
469
|
+
const retryOptions: RetryOptions = {
|
|
470
|
+
type: 'exponentialBackoff',
|
|
471
|
+
maxAttempts: 80,
|
|
472
|
+
initialDelay: 30,
|
|
473
|
+
backoffMultiplier: 1.3,
|
|
474
|
+
jitter: 0.2,
|
|
475
|
+
maxDelay: 500,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
await retry(
|
|
479
|
+
() => withTimeout(this.ping(), 500),
|
|
480
|
+
retryOptions,
|
|
481
|
+
() => {
|
|
482
|
+
const protocol = this._wireProto === 'grpc' ? 'rest' : 'grpc';
|
|
483
|
+
this.initWireConnection(protocol);
|
|
484
|
+
return true;
|
|
485
|
+
});
|
|
445
486
|
}
|
|
446
487
|
|
|
447
488
|
public async license(): Promise<grpcTypes.MaintenanceAPI_License_Response> {
|
|
@@ -449,7 +490,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
449
490
|
if (cl instanceof GrpcPlApiClient) {
|
|
450
491
|
return (await cl.license({})).response;
|
|
451
492
|
} else {
|
|
452
|
-
const resp = notEmpty((await cl.GET('/v1/license')).data);
|
|
493
|
+
const resp = notEmpty((await cl.GET('/v1/license')).data, 'REST: empty response for license request');
|
|
453
494
|
return {
|
|
454
495
|
status: resp.status,
|
|
455
496
|
isOk: resp.isOk,
|
|
@@ -463,7 +504,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
463
504
|
if (cl instanceof GrpcPlApiClient) {
|
|
464
505
|
return (await cl.authMethods({})).response;
|
|
465
506
|
} else {
|
|
466
|
-
return notEmpty((await cl.GET('/v1/auth/methods')).data);
|
|
507
|
+
return notEmpty((await cl.GET('/v1/auth/methods')).data, 'REST: empty response for auth methods request');
|
|
467
508
|
}
|
|
468
509
|
}
|
|
469
510
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { UnauthenticatedPlClient } from './unauth_client';
|
|
2
|
-
import { getTestConfig } from '../test/test_config';
|
|
2
|
+
import { getTestConfig, plAddressToTestConfig } from '../test/test_config';
|
|
3
3
|
import { UnauthenticatedError } from './errors';
|
|
4
4
|
import { test, expect } from 'vitest';
|
|
5
5
|
|
|
6
6
|
test('ping test', async () => {
|
|
7
|
-
const client =
|
|
7
|
+
const client = await UnauthenticatedPlClient.build(plAddressToTestConfig(getTestConfig().address));
|
|
8
8
|
const response = await client.ping();
|
|
9
9
|
expect(response).toHaveProperty('coreVersion');
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
test('get auth methods', async () => {
|
|
13
|
-
const client =
|
|
13
|
+
const client = await UnauthenticatedPlClient.build(plAddressToTestConfig(getTestConfig().address));
|
|
14
14
|
const response = await client.authMethods();
|
|
15
15
|
expect(response).toHaveProperty('methods');
|
|
16
16
|
});
|
|
@@ -21,7 +21,7 @@ test('wrong login', async () => {
|
|
|
21
21
|
console.log('skipped');
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
const client =
|
|
24
|
+
const client = await UnauthenticatedPlClient.build(plAddressToTestConfig(testConfig.address));
|
|
25
25
|
await expect(client.login(testConfig.test_user, testConfig.test_password + 'A')).rejects.toThrow(
|
|
26
26
|
UnauthenticatedError
|
|
27
27
|
);
|
|
@@ -11,8 +11,13 @@ import { UnauthenticatedError } from './errors';
|
|
|
11
11
|
export class UnauthenticatedPlClient {
|
|
12
12
|
public readonly ll: LLPlClient;
|
|
13
13
|
|
|
14
|
-
constructor(
|
|
15
|
-
this.ll =
|
|
14
|
+
private constructor(ll: LLPlClient) {
|
|
15
|
+
this.ll = ll;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static async build(configOrAddress: PlClientConfig | string): Promise<UnauthenticatedPlClient> {
|
|
19
|
+
const ll = await LLPlClient.build(configOrAddress);
|
|
20
|
+
return new UnauthenticatedPlClient(ll);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
public async ping(): Promise<MaintenanceAPI_Ping_Response> {
|
|
@@ -3,6 +3,7 @@ import type { BiDiStream } from './abstract_stream';
|
|
|
3
3
|
import Denque from 'denque';
|
|
4
4
|
import type { RetryConfig } from '../helpers/retry_strategy';
|
|
5
5
|
import { RetryStrategy } from '../helpers/retry_strategy';
|
|
6
|
+
import { DisconnectedError } from './errors';
|
|
6
7
|
|
|
7
8
|
interface QueuedMessage<InType extends object> {
|
|
8
9
|
message: InType;
|
|
@@ -183,6 +184,18 @@ export class WebSocketBiDiStream<ClientMsg extends object, ServerMsg extends obj
|
|
|
183
184
|
private onClose(): void {
|
|
184
185
|
this.progressConnectionState(ConnectionState.CLOSED);
|
|
185
186
|
|
|
187
|
+
// If abort signal was triggered, use that as the error source
|
|
188
|
+
if (this.options.abortSignal?.aborted && !this.lastError) {
|
|
189
|
+
const reason = this.options.abortSignal.reason;
|
|
190
|
+
if (reason instanceof Error) {
|
|
191
|
+
this.lastError = reason;
|
|
192
|
+
} else if (reason !== undefined) {
|
|
193
|
+
this.lastError = new Error(String(reason), { cause: reason });
|
|
194
|
+
} else {
|
|
195
|
+
this.lastError = this.createStreamClosedError();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
186
199
|
if (!this.lastError) {
|
|
187
200
|
this.rejectAllSendOperations(this.createStreamClosedError());
|
|
188
201
|
this.resolveAllPendingResponses(); // unblock active async iterator
|
|
@@ -380,7 +393,15 @@ export class WebSocketBiDiStream<ClientMsg extends object, ServerMsg extends obj
|
|
|
380
393
|
|
|
381
394
|
private toError(error: unknown): Error {
|
|
382
395
|
if (error instanceof Error) return error;
|
|
383
|
-
if (error instanceof ErrorEvent)
|
|
396
|
+
if (error instanceof ErrorEvent) {
|
|
397
|
+
const err = error.error;
|
|
398
|
+
// undici WebSocket throws TypeError with empty message on socket close
|
|
399
|
+
// (e.g., when connection is lost or server disconnects)
|
|
400
|
+
if (err instanceof TypeError && !err.message) {
|
|
401
|
+
return new DisconnectedError('WebSocket connection closed unexpectedly');
|
|
402
|
+
}
|
|
403
|
+
return err instanceof Error ? err : new Error('WebSocket error', { cause: error });
|
|
404
|
+
}
|
|
384
405
|
return new Error(String(error));
|
|
385
406
|
}
|
|
386
407
|
|
package/src/proto-rest/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { paths as PlApiPaths } from './plapi';
|
|
8
8
|
import { default as createOpenApiClient, type Middleware, type Client } from 'openapi-fetch';
|
|
9
9
|
import { Dispatcher, fetch as undiciFetch } from 'undici';
|
|
10
|
-
import { rethrowMeaningfulError } from '../core/errors';
|
|
10
|
+
import { RESTError, rethrowMeaningfulError } from '../core/errors';
|
|
11
11
|
import { Code } from '../proto-grpc/google/rpc/code';
|
|
12
12
|
|
|
13
13
|
export { PlApiPaths };
|
|
@@ -92,7 +92,7 @@ function errorHandlerMiddleware(): Middleware {
|
|
|
92
92
|
throw new Error(respErr.error);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
rethrowMeaningfulError(respErr.error);
|
|
95
|
+
rethrowMeaningfulError(new RESTError(respErr.error));
|
|
96
96
|
},
|
|
97
97
|
};
|
|
98
98
|
}
|