@milaboratories/pl-client 2.16.12 → 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 (74) 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/driver.cjs +1 -1
  14. package/dist/core/driver.cjs.map +1 -1
  15. package/dist/core/driver.js +1 -1
  16. package/dist/core/driver.js.map +1 -1
  17. package/dist/core/errors.cjs +15 -4
  18. package/dist/core/errors.cjs.map +1 -1
  19. package/dist/core/errors.d.ts.map +1 -1
  20. package/dist/core/errors.js +15 -4
  21. package/dist/core/errors.js.map +1 -1
  22. package/dist/core/ll_client.cjs +61 -21
  23. package/dist/core/ll_client.cjs.map +1 -1
  24. package/dist/core/ll_client.d.ts +12 -3
  25. package/dist/core/ll_client.d.ts.map +1 -1
  26. package/dist/core/ll_client.js +62 -22
  27. package/dist/core/ll_client.js.map +1 -1
  28. package/dist/core/transaction.cjs +1 -1
  29. package/dist/core/transaction.js +1 -1
  30. package/dist/core/unauth_client.cjs +6 -2
  31. package/dist/core/unauth_client.cjs.map +1 -1
  32. package/dist/core/unauth_client.d.ts +2 -1
  33. package/dist/core/unauth_client.d.ts.map +1 -1
  34. package/dist/core/unauth_client.js +6 -2
  35. package/dist/core/unauth_client.js.map +1 -1
  36. package/dist/core/websocket_stream.cjs +147 -129
  37. package/dist/core/websocket_stream.cjs.map +1 -1
  38. package/dist/core/websocket_stream.d.ts +29 -22
  39. package/dist/core/websocket_stream.d.ts.map +1 -1
  40. package/dist/core/websocket_stream.js +148 -130
  41. package/dist/core/websocket_stream.js.map +1 -1
  42. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +136 -0
  43. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
  44. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +75 -1
  45. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
  46. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +135 -1
  47. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
  48. package/dist/proto-rest/index.cjs +16 -2
  49. package/dist/proto-rest/index.cjs.map +1 -1
  50. package/dist/proto-rest/index.d.ts.map +1 -1
  51. package/dist/proto-rest/index.js +16 -2
  52. package/dist/proto-rest/index.js.map +1 -1
  53. package/dist/test/test_config.cjs +13 -3
  54. package/dist/test/test_config.cjs.map +1 -1
  55. package/dist/test/test_config.d.ts +4 -0
  56. package/dist/test/test_config.d.ts.map +1 -1
  57. package/dist/test/test_config.js +12 -4
  58. package/dist/test/test_config.js.map +1 -1
  59. package/package.json +6 -6
  60. package/src/core/client.ts +40 -21
  61. package/src/core/default_client.ts +1 -1
  62. package/src/core/driver.ts +1 -1
  63. package/src/core/errors.ts +14 -4
  64. package/src/core/ll_client.test.ts +9 -3
  65. package/src/core/ll_client.ts +81 -26
  66. package/src/core/unauth_client.test.ts +4 -4
  67. package/src/core/unauth_client.ts +7 -2
  68. package/src/core/websocket_stream.test.ts +19 -8
  69. package/src/core/websocket_stream.ts +173 -164
  70. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +179 -1
  71. package/src/proto-rest/index.ts +17 -2
  72. package/src/test/test_config.ts +13 -4
  73. /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
  74. /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
@@ -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';
@@ -29,7 +29,7 @@ test('unauthenticated status change', async () => {
29
29
  return;
30
30
  }
31
31
 
32
- const client = new LLPlClient(cfg.address);
32
+ const client = await LLPlClient.build(plAddressToTestConfig(cfg.address));
33
33
  expect(client.status).toBe('OK');
34
34
 
35
35
  const tx = client.createTx(true);
@@ -54,10 +54,16 @@ test('unauthenticated status change', async () => {
54
54
  });
55
55
 
56
56
  test('automatic token update', async () => {
57
+ const cfg = getTestConfig();
58
+ if (cfg.test_password === undefined) {
59
+ console.log("skipping test because target server doesn't support authentication");
60
+ return;
61
+ }
62
+
57
63
  const { conf, auth } = await getTestClientConf();
58
64
  conf.authMaxRefreshSeconds = 1;
59
65
  let numberOfAuthUpdates = 0;
60
- const client = new LLPlClient(conf, {
66
+ const client = await LLPlClient.build(conf, {
61
67
  auth: {
62
68
  authInformation: auth.authInformation,
63
69
  onUpdate: (auth) => {
@@ -26,9 +26,10 @@ 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
+ import { TxAPI_ClientMessage, TxAPI_ServerMessage } from '../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api';
32
33
 
33
34
  export interface PlCallOps {
34
35
  timeout?: number;
@@ -53,8 +54,6 @@ class WireClientProviderImpl<Client> implements WireClientProvider<Client> {
53
54
 
54
55
  /** Abstract out low level networking and authorization details */
55
56
  export class LLPlClient implements WireClientProviderFactory {
56
- public readonly conf: PlClientConfig;
57
-
58
57
  /** Initial authorization information */
59
58
  private authInformation?: AuthInformation;
60
59
  /** Will be executed by the client when it is required */
@@ -69,7 +68,7 @@ export class LLPlClient implements WireClientProviderFactory {
69
68
  private _status: PlConnectionStatus = 'OK';
70
69
  private readonly statusListener?: PlConnectionStatusListener;
71
70
 
72
- private _wireProto: wireProtocol | undefined = undefined;
71
+ private _wireProto: wireProtocol = 'grpc';
73
72
  private _wireConn!: WireConnection;
74
73
 
75
74
  private readonly _restInterceptors: Dispatcher.DispatcherComposeInterceptor[];
@@ -81,18 +80,29 @@ export class LLPlClient implements WireClientProviderFactory {
81
80
 
82
81
  public readonly httpDispatcher: Dispatcher;
83
82
 
84
- constructor(
83
+ public static async build(
85
84
  configOrAddress: PlClientConfig | string,
86
- private readonly ops: {
85
+ ops: {
87
86
  auth?: AuthOps;
88
87
  statusListener?: PlConnectionStatusListener;
89
88
  shouldUseGzip?: boolean;
90
89
  } = {},
91
90
  ) {
92
- this.conf = typeof configOrAddress === 'string'
93
- ? plAddressToConfig(configOrAddress)
94
- : 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
+ }
95
97
 
98
+ private constructor(
99
+ public readonly conf: PlClientConfig,
100
+ private readonly ops: {
101
+ auth?: AuthOps;
102
+ statusListener?: PlConnectionStatusListener;
103
+ shouldUseGzip?: boolean;
104
+ } = {},
105
+ ) {
96
106
  const { auth, statusListener } = ops;
97
107
 
98
108
  if (auth !== undefined) {
@@ -119,8 +129,11 @@ export class LLPlClient implements WireClientProviderFactory {
119
129
  this._grpcInterceptors.push(this.createGrpcErrorInterceptor());
120
130
 
121
131
  this.httpDispatcher = defaultHttpDispatcher(this.conf.httpProxy);
132
+ if (this.conf.wireProtocol) {
133
+ this._wireProto = this.conf.wireProtocol;
134
+ }
122
135
 
123
- this.initWireConnection();
136
+ this.initWireConnection(this._wireProto);
124
137
 
125
138
  if (statusListener !== undefined) {
126
139
  this.statusListener = statusListener;
@@ -141,13 +154,8 @@ export class LLPlClient implements WireClientProviderFactory {
141
154
  });
142
155
  }
143
156
 
144
- private initWireConnection() {
145
- if (this._wireProto === undefined) {
146
- // TODO: implement automatic server mode detection
147
- this._wireProto = this.conf.wireProtocol ?? 'grpc';
148
- }
149
-
150
- switch (this._wireProto) {
157
+ private initWireConnection(protocol: wireProtocol) {
158
+ switch (protocol) {
151
159
  case 'rest':
152
160
  this.initRestConnection();
153
161
  return;
@@ -157,12 +165,12 @@ export class LLPlClient implements WireClientProviderFactory {
157
165
  default:
158
166
  ((v: never) => {
159
167
  throw new Error(`Unsupported wire protocol '${v as string}'. Use one of: ${SUPPORTED_WIRE_PROTOCOLS.join(', ')}`);
160
- })(this._wireProto);
168
+ })(protocol);
161
169
  }
162
170
  }
163
171
 
164
172
  private initRestConnection(): void {
165
- const dispatcher = defaultHttpDispatcher(this.conf.httpProxy, this._restInterceptors);
173
+ const dispatcher = defaultHttpDispatcher(this.conf.grpcProxy, this._restInterceptors);
166
174
  this._replaceWireConnection({ type: 'rest', Config: this.conf, Dispatcher: dispatcher, Middlewares: this._restMiddlewares });
167
175
  }
168
176
 
@@ -220,6 +228,7 @@ export class LLPlClient implements WireClientProviderFactory {
220
228
  private _replaceWireConnection(newConn: WireConnection): void {
221
229
  const oldConn = this._wireConn;
222
230
  this._wireConn = newConn;
231
+ this._wireProto = newConn.type;
223
232
 
224
233
  // Reset all providers to let them reinitialize their clients
225
234
  for (let i = 0; i < this.providers.length; i++) {
@@ -268,6 +277,10 @@ export class LLPlClient implements WireClientProviderFactory {
268
277
  return this._wireConn;
269
278
  }
270
279
 
280
+ public get wireProtocol(): wireProtocol | undefined {
281
+ return this._wireProto;
282
+ }
283
+
271
284
  /** Returns true if client is authenticated. Even with anonymous auth information
272
285
  * connection is considered authenticated. Unauthenticated clients are used for
273
286
  * login and similar tasks, see {@link UnauthenticatedPlClient}. */
@@ -443,6 +456,32 @@ export class LLPlClient implements WireClientProviderFactory {
443
456
  }
444
457
  }
445
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
+
446
485
  public async license(): Promise<grpcTypes.MaintenanceAPI_License_Response> {
447
486
  const cl = this.clientProvider.get();
448
487
  if (cl instanceof GrpcPlApiClient) {
@@ -490,22 +529,38 @@ export class LLPlClient implements WireClientProviderFactory {
490
529
  });
491
530
  }
492
531
 
493
- if (this._wireProto === 'rest') {
532
+ const wireConn = this.wireConnection;
533
+ if (wireConn.type === 'rest') {
494
534
  // For REST/WebSocket protocol, timeout needs to be converted to AbortSignal
495
535
  if (timeout !== undefined) {
496
536
  totalAbortSignal = AbortSignal.any([totalAbortSignal, AbortSignal.timeout(timeout)]);
497
537
  }
538
+
539
+ // The gRPC transport has the auth interceptor that already handles it, but here we need to refresh the auth information to be safe.
540
+ this.refreshAuthInformationIfNeeded();
541
+
498
542
  const wsUrl = this.conf.ssl
499
543
  ? `wss://${this.conf.hostAndPort}/v1/ws/tx`
500
544
  : `ws://${this.conf.hostAndPort}/v1/ws/tx`;
501
545
 
502
- // The gRPC transport has the auth interceptor that already handles it, so we need to refresh the auth information here.
503
- this.refreshAuthInformationIfNeeded();
504
- const jwtToken = this.authInformation?.jwtToken;
505
-
506
- return new WebSocketBiDiStream(wsUrl, totalAbortSignal, jwtToken);
546
+ return new WebSocketBiDiStream(wsUrl,
547
+ (msg) => TxAPI_ClientMessage.toBinary(msg),
548
+ (data) => TxAPI_ServerMessage.fromBinary(new Uint8Array(data)),
549
+ {
550
+ abortSignal: totalAbortSignal,
551
+ jwtToken: this.authInformation?.jwtToken,
552
+ dispatcher: wireConn.Dispatcher,
553
+
554
+ onComplete: async (stream) => stream.requests.send({
555
+ // Ask server to gracefully close the stream on its side, if not done yet.
556
+ requestId: 0,
557
+ request: { oneofKind: 'streamClose', streamClose: {} },
558
+ }),
559
+ },
560
+ );
507
561
  }
508
- throw new Error('tx is not supported for this wire protocol');
562
+
563
+ throw new Error(`transactions are not supported for wire protocol ${this._wireProto}`);
509
564
  });
510
565
  }
511
566
 
@@ -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> {
@@ -82,7 +82,7 @@ import type { RetryConfig } from '../helpers/retry_strategy';
82
82
  type MockWS = InstanceType<typeof MockWebSocket>;
83
83
 
84
84
  interface StreamContext {
85
- stream: WebSocketBiDiStream;
85
+ stream: WebSocketBiDiStream<ClientMessageType, ServerMessageType>;
86
86
  ws: MockWS;
87
87
  controller: AbortController;
88
88
  }
@@ -91,9 +91,13 @@ function createStream(token?: string, retryConfig?: Partial<RetryConfig>): Strea
91
91
  const controller = new AbortController();
92
92
  const stream = new WebSocketBiDiStream(
93
93
  'ws://localhost:8080',
94
- controller.signal,
95
- token,
96
- retryConfig,
94
+ (message: ClientMessageType) => ClientMessageType.toBinary(message),
95
+ (data) => ServerMessageType.fromBinary(data),
96
+ {
97
+ abortSignal: controller.signal,
98
+ jwtToken: token,
99
+ retryConfig: retryConfig,
100
+ },
97
101
  );
98
102
  const ws = MockWebSocket.instances[MockWebSocket.instances.length - 1];
99
103
  return { stream, ws, controller };
@@ -115,7 +119,7 @@ function createClientMessage(): ClientMessageType {
115
119
  }
116
120
 
117
121
  async function collectMessages(
118
- stream: WebSocketBiDiStream,
122
+ stream: WebSocketBiDiStream<ClientMessageType, ServerMessageType>,
119
123
  count: number,
120
124
  ): Promise<ServerMessageType[]> {
121
125
  const messages: ServerMessageType[] = [];
@@ -149,7 +153,14 @@ describe('WebSocketBiDiStream', () => {
149
153
  const controller = new AbortController();
150
154
  controller.abort();
151
155
 
152
- new WebSocketBiDiStream('ws://localhost:8080', controller.signal);
156
+ new WebSocketBiDiStream(
157
+ 'ws://localhost:8080',
158
+ (message: ClientMessageType) => ClientMessageType.toBinary(message),
159
+ (data) => ServerMessageType.fromBinary(data),
160
+ {
161
+ abortSignal: controller.signal,
162
+ },
163
+ );
153
164
 
154
165
  expect(MockWebSocket.instances).toHaveLength(0);
155
166
  });
@@ -332,7 +343,7 @@ describe('WebSocketBiDiStream', () => {
332
343
  maxAttempts: 5,
333
344
  };
334
345
 
335
- test('should attempt reconnection on unexpected close', async () => {
346
+ test('should not attempt reconnection on unexpected close', async () => {
336
347
  const { ws } = createStream(undefined, retryConfig);
337
348
 
338
349
  await openConnection(ws);
@@ -341,7 +352,7 @@ describe('WebSocketBiDiStream', () => {
341
352
  ws.emit('close');
342
353
  await vi.advanceTimersByTimeAsync(150);
343
354
 
344
- expect(MockWebSocket.instances.length).toBeGreaterThan(1);
355
+ expect(MockWebSocket.instances.length).toBe(1);
345
356
  });
346
357
 
347
358
  test('should stop reconnecting after max attempts', async () => {