@milaboratories/pl-client 2.16.13 → 2.16.14

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.
Files changed (54) hide show
  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.cjs.map +1 -1
  2. 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
  3. package/dist/core/client.cjs +31 -16
  4. package/dist/core/client.cjs.map +1 -1
  5. package/dist/core/client.d.ts +3 -2
  6. package/dist/core/client.d.ts.map +1 -1
  7. package/dist/core/client.js +31 -16
  8. package/dist/core/client.js.map +1 -1
  9. package/dist/core/default_client.cjs +1 -1
  10. package/dist/core/default_client.cjs.map +1 -1
  11. package/dist/core/default_client.js +1 -1
  12. package/dist/core/default_client.js.map +1 -1
  13. package/dist/core/errors.cjs +15 -4
  14. package/dist/core/errors.cjs.map +1 -1
  15. package/dist/core/errors.d.ts.map +1 -1
  16. package/dist/core/errors.js +15 -4
  17. package/dist/core/errors.js.map +1 -1
  18. package/dist/core/ll_client.cjs +44 -14
  19. package/dist/core/ll_client.cjs.map +1 -1
  20. package/dist/core/ll_client.d.ts +12 -3
  21. package/dist/core/ll_client.d.ts.map +1 -1
  22. package/dist/core/ll_client.js +45 -15
  23. package/dist/core/ll_client.js.map +1 -1
  24. package/dist/core/transaction.cjs +1 -1
  25. package/dist/core/transaction.js +1 -1
  26. package/dist/core/unauth_client.cjs +6 -2
  27. package/dist/core/unauth_client.cjs.map +1 -1
  28. package/dist/core/unauth_client.d.ts +2 -1
  29. package/dist/core/unauth_client.d.ts.map +1 -1
  30. package/dist/core/unauth_client.js +6 -2
  31. package/dist/core/unauth_client.js.map +1 -1
  32. package/dist/core/websocket_stream.cjs +23 -2
  33. package/dist/core/websocket_stream.cjs.map +1 -1
  34. package/dist/core/websocket_stream.d.ts.map +1 -1
  35. package/dist/core/websocket_stream.js +23 -2
  36. package/dist/core/websocket_stream.js.map +1 -1
  37. package/dist/test/test_config.cjs +13 -3
  38. package/dist/test/test_config.cjs.map +1 -1
  39. package/dist/test/test_config.d.ts +4 -0
  40. package/dist/test/test_config.d.ts.map +1 -1
  41. package/dist/test/test_config.js +12 -4
  42. package/dist/test/test_config.js.map +1 -1
  43. package/package.json +4 -4
  44. package/src/core/client.ts +40 -21
  45. package/src/core/default_client.ts +1 -1
  46. package/src/core/errors.ts +14 -4
  47. package/src/core/ll_client.test.ts +9 -3
  48. package/src/core/ll_client.ts +56 -18
  49. package/src/core/unauth_client.test.ts +4 -4
  50. package/src/core/unauth_client.ts +7 -2
  51. package/src/core/websocket_stream.ts +22 -1
  52. package/src/test/test_config.ts +13 -4
  53. /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
  54. /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
@@ -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 | undefined = undefined;
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
- constructor(
83
+ public static async build(
86
84
  configOrAddress: PlClientConfig | string,
87
- private readonly ops: {
85
+ ops: {
88
86
  auth?: AuthOps;
89
87
  statusListener?: PlConnectionStatusListener;
90
88
  shouldUseGzip?: boolean;
91
89
  } = {},
92
90
  ) {
93
- this.conf = typeof configOrAddress === 'string'
94
- ? plAddressToConfig(configOrAddress)
95
- : configOrAddress;
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
- if (this._wireProto === undefined) {
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
- })(this._wireProto);
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}. */
@@ -444,6 +456,32 @@ export class LLPlClient implements WireClientProviderFactory {
444
456
  }
445
457
  }
446
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;
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(() => withTimeout(this.ping(), 500), retryOptions, () => {
479
+ const protocol = this._wireProto === 'grpc' ? 'rest' : 'grpc';
480
+ this.initWireConnection(protocol);
481
+ return true;
482
+ });
483
+ }
484
+
447
485
  public async license(): Promise<grpcTypes.MaintenanceAPI_License_Response> {
448
486
  const cl = this.clientProvider.get();
449
487
  if (cl instanceof GrpcPlApiClient) {
@@ -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 = new UnauthenticatedPlClient(getTestConfig().address);
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 = new UnauthenticatedPlClient(getTestConfig().address);
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 = new UnauthenticatedPlClient(testConfig.address);
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(configOrAddress: PlClientConfig | string) {
15
- this.ll = new LLPlClient(configOrAddress);
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) return error.error;
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
 
@@ -54,6 +54,16 @@ export function getTestConfig(): TestConfig {
54
54
  return conf as TestConfig;
55
55
  }
56
56
 
57
+ /** Default request timeout for tests (ms) */
58
+ export const TEST_REQUEST_TIMEOUT = 500;
59
+
60
+ /** Returns PlClientConfig with reduced timeout for tests */
61
+ export function plAddressToTestConfig(address: string): PlClientConfig {
62
+ const plConf = plAddressToConfig(address);
63
+ plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;
64
+ return plConf;
65
+ }
66
+
57
67
  interface AuthCache {
58
68
  /** To check if config changed */
59
69
  conf: TestConfig;
@@ -109,9 +119,8 @@ export async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth:
109
119
  }
110
120
  }
111
121
 
112
- const plConf = plAddressToConfig(tConf.address);
113
-
114
- const uClient = new UnauthenticatedPlClient(plConf);
122
+ const plConf = plAddressToTestConfig(tConf.address);
123
+ const uClient = await UnauthenticatedPlClient.build(plConf);
115
124
 
116
125
  const requireAuth = await uClient.requireAuth();
117
126
 
@@ -147,7 +156,7 @@ export async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth:
147
156
 
148
157
  export async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {
149
158
  const { conf, auth } = await getTestClientConf();
150
- return new LLPlClient({ ...conf, ...confOverrides }, { auth });
159
+ return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });
151
160
  }
152
161
 
153
162
  export async function getTestClient(