@milaboratories/pl-client 2.4.10

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 (64) hide show
  1. package/README.md +52 -0
  2. package/dist/index.cjs +14527 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.js +14426 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +49 -0
  7. package/src/core/auth.ts +27 -0
  8. package/src/core/client.test.ts +47 -0
  9. package/src/core/client.ts +302 -0
  10. package/src/core/config.test.ts +19 -0
  11. package/src/core/config.ts +197 -0
  12. package/src/core/default_client.ts +161 -0
  13. package/src/core/driver.ts +30 -0
  14. package/src/core/error.test.ts +14 -0
  15. package/src/core/errors.ts +84 -0
  16. package/src/core/http.ts +178 -0
  17. package/src/core/ll_client.test.ts +111 -0
  18. package/src/core/ll_client.ts +228 -0
  19. package/src/core/ll_transaction.test.ts +152 -0
  20. package/src/core/ll_transaction.ts +333 -0
  21. package/src/core/transaction.test.ts +173 -0
  22. package/src/core/transaction.ts +730 -0
  23. package/src/core/type_conversion.ts +121 -0
  24. package/src/core/types.test.ts +22 -0
  25. package/src/core/types.ts +223 -0
  26. package/src/core/unauth_client.test.ts +21 -0
  27. package/src/core/unauth_client.ts +48 -0
  28. package/src/helpers/pl.ts +141 -0
  29. package/src/helpers/poll.ts +178 -0
  30. package/src/helpers/rich_resource_types.test.ts +22 -0
  31. package/src/helpers/rich_resource_types.ts +84 -0
  32. package/src/helpers/smart_accessors.ts +146 -0
  33. package/src/helpers/state_helpers.ts +5 -0
  34. package/src/helpers/tx_helpers.ts +24 -0
  35. package/src/index.ts +14 -0
  36. package/src/proto/github.com/googleapis/googleapis/google/rpc/status.ts +125 -0
  37. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.ts +45 -0
  38. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.ts +271 -0
  39. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.client.ts +51 -0
  40. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.ts +380 -0
  41. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.ts +59 -0
  42. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.ts +450 -0
  43. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.ts +148 -0
  44. package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.ts +706 -0
  45. package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +406 -0
  46. package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api.ts +12636 -0
  47. package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api_types.ts +1384 -0
  48. package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/base_types.ts +181 -0
  49. package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/import.ts +251 -0
  50. package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/resource_types.ts +693 -0
  51. package/src/proto/google/api/http.ts +687 -0
  52. package/src/proto/google/protobuf/any.ts +326 -0
  53. package/src/proto/google/protobuf/descriptor.ts +4502 -0
  54. package/src/proto/google/protobuf/duration.ts +230 -0
  55. package/src/proto/google/protobuf/empty.ts +81 -0
  56. package/src/proto/google/protobuf/struct.ts +482 -0
  57. package/src/proto/google/protobuf/timestamp.ts +287 -0
  58. package/src/proto/google/protobuf/wrappers.ts +751 -0
  59. package/src/test/test_config.test.ts +6 -0
  60. package/src/test/test_config.ts +166 -0
  61. package/src/util/branding.ts +4 -0
  62. package/src/util/pl.ts +11 -0
  63. package/src/util/util.test.ts +10 -0
  64. package/src/util/util.ts +9 -0
@@ -0,0 +1,228 @@
1
+ import { PlatformClient } from '../proto/github.com/milaboratory/pl/plapi/plapiproto/api.client';
2
+ import {
3
+ ChannelCredentials,
4
+ InterceptingCall,
5
+ Interceptor,
6
+ status as GrpcStatus
7
+ } from '@grpc/grpc-js';
8
+ import {
9
+ AuthInformation,
10
+ AuthOps,
11
+ plAddressToConfig,
12
+ PlClientConfig,
13
+ PlConnectionStatus,
14
+ PlConnectionStatusListener
15
+ } from './config';
16
+ import { GrpcOptions, GrpcTransport } from '@protobuf-ts/grpc-transport';
17
+ import { LLPlTransaction } from './ll_transaction';
18
+ import { parsePlJwt } from '../util/pl';
19
+ import { Agent, Dispatcher, ProxyAgent } from 'undici';
20
+ import { inferAuthRefreshTime } from './auth';
21
+
22
+ export interface PlCallOps {
23
+ timeout?: number;
24
+ abortSignal?: AbortSignal;
25
+ }
26
+
27
+ /** Abstract out low level networking and authorization details */
28
+ export class LLPlClient {
29
+ public readonly conf: PlClientConfig;
30
+
31
+ /** Initial authorization information */
32
+ private authInformation?: AuthInformation;
33
+ /** Will be executed by the client when it is required */
34
+ private readonly onAuthUpdate?: (newInfo: AuthInformation) => void;
35
+ /** Will be executed if auth-related error happens during normal client operation */
36
+ private readonly onAuthError?: () => void;
37
+ /** Will be executed by the client when it is required */
38
+ private readonly onAuthRefreshProblem?: (error: unknown) => void;
39
+ /** Threshold after which auth info refresh is required */
40
+ private refreshTimestamp?: number;
41
+
42
+ private _status: PlConnectionStatus = 'OK';
43
+ private readonly statusListener?: PlConnectionStatusListener;
44
+
45
+ public readonly grpcTransport: GrpcTransport;
46
+ public readonly grpcPl: PlatformClient;
47
+
48
+ public readonly httpDispatcher: Dispatcher;
49
+
50
+ constructor(
51
+ configOrAddress: PlClientConfig | string,
52
+ ops: {
53
+ auth?: AuthOps;
54
+ statusListener?: PlConnectionStatusListener;
55
+ } = {}
56
+ ) {
57
+ this.conf =
58
+ typeof configOrAddress === 'string' ? plAddressToConfig(configOrAddress) : configOrAddress;
59
+
60
+ const grpcInterceptors: Interceptor[] = [];
61
+
62
+ const { auth, statusListener } = ops;
63
+
64
+ if (auth !== undefined) {
65
+ this.refreshTimestamp = inferAuthRefreshTime(
66
+ auth.authInformation,
67
+ this.conf.authMaxRefreshSeconds
68
+ );
69
+ grpcInterceptors.push(this.createAuthInterceptor());
70
+ this.authInformation = auth.authInformation;
71
+ this.onAuthUpdate = auth.onUpdate;
72
+ this.onAuthRefreshProblem = auth.onUpdateError;
73
+ this.onAuthError = auth.onAuthError;
74
+ }
75
+
76
+ grpcInterceptors.push(this.createErrorInterceptor());
77
+
78
+ const grpcOptions: GrpcOptions = {
79
+ host: this.conf.hostAndPort,
80
+ timeout: this.conf.defaultRequestTimeout,
81
+ channelCredentials: this.conf.ssl
82
+ ? ChannelCredentials.createSsl()
83
+ : ChannelCredentials.createInsecure(),
84
+ clientOptions: {
85
+ 'grpc.use_local_subchannel_pool': 1,
86
+ interceptors: grpcInterceptors
87
+ }
88
+ };
89
+
90
+ if (this.conf.grpcProxy) process.env.grpc_proxy = this.conf.grpcProxy;
91
+ else delete process.env.grpc_proxy;
92
+
93
+ this.grpcTransport = new GrpcTransport(grpcOptions);
94
+ this.grpcPl = new PlatformClient(this.grpcTransport);
95
+
96
+ // setting up http(s)
97
+ if (this.conf.httpProxy !== undefined)
98
+ this.httpDispatcher = new ProxyAgent(this.conf.httpProxy);
99
+ else this.httpDispatcher = new Agent();
100
+
101
+ if (statusListener !== undefined) {
102
+ this.statusListener = statusListener;
103
+ statusListener(this._status);
104
+ }
105
+ }
106
+
107
+ /** Returns true if client is authenticated. Even with anonymous auth information
108
+ * connection is considered authenticated. Unauthenticated clients are used for
109
+ * login and similar tasks, see {@link UnauthenticatedPlClient}. */
110
+ public get authenticated(): boolean {
111
+ return this.authInformation !== undefined;
112
+ }
113
+
114
+ /** null means anonymous connection */
115
+ public get authUser(): string | null {
116
+ if (!this.authenticated) throw new Error('Client is not authenticated');
117
+ if (this.authInformation?.jwtToken)
118
+ return parsePlJwt(this.authInformation?.jwtToken).user.login;
119
+ else return null;
120
+ }
121
+
122
+ private updateStatus(newStatus: PlConnectionStatus) {
123
+ process.nextTick(() => {
124
+ if (this._status !== newStatus) {
125
+ this._status = newStatus;
126
+ if (this.statusListener !== undefined) this.statusListener(this._status);
127
+ if (this.onAuthError !== undefined) this.onAuthError();
128
+ }
129
+ });
130
+ }
131
+
132
+ public get status(): PlConnectionStatus {
133
+ return this._status;
134
+ }
135
+
136
+ private authRefreshInProgress: boolean = false;
137
+
138
+ private refreshAuthInformationIfNeeded(): void {
139
+ if (
140
+ this.refreshTimestamp === undefined ||
141
+ Date.now() < this.refreshTimestamp ||
142
+ this.authRefreshInProgress ||
143
+ this._status === 'Unauthenticated'
144
+ )
145
+ return;
146
+
147
+ // Running refresh in background
148
+ this.authRefreshInProgress = true;
149
+ (async () => {
150
+ try {
151
+ const response = await this.grpcPl.getJWTToken({
152
+ expiration: {
153
+ seconds: BigInt(this.conf.authTTLSeconds),
154
+ nanos: 0
155
+ }
156
+ }).response;
157
+ this.authInformation = { jwtToken: response.token };
158
+ this.refreshTimestamp = inferAuthRefreshTime(
159
+ this.authInformation,
160
+ this.conf.authMaxRefreshSeconds
161
+ );
162
+ if (this.onAuthUpdate) this.onAuthUpdate(this.authInformation);
163
+ } catch (e: unknown) {
164
+ if (this.onAuthRefreshProblem) this.onAuthRefreshProblem(e);
165
+ } finally {
166
+ this.authRefreshInProgress = false;
167
+ }
168
+ })();
169
+ }
170
+
171
+ /** Detects certain errors and update client status accordingly */
172
+ private createErrorInterceptor(): Interceptor {
173
+ return (options, nextCall) => {
174
+ return new InterceptingCall(nextCall(options), {
175
+ start: (metadata, listener, next) => {
176
+ next(metadata, {
177
+ onReceiveStatus: (status, next) => {
178
+ if (status.code == GrpcStatus.UNAUTHENTICATED)
179
+ // (!!!) don't change to "==="
180
+ this.updateStatus('Unauthenticated');
181
+ if (status.code == GrpcStatus.UNAVAILABLE)
182
+ // (!!!) don't change to "==="
183
+ this.updateStatus('Disconnected');
184
+ next(status);
185
+ }
186
+ });
187
+ }
188
+ });
189
+ };
190
+ }
191
+
192
+ /** Injects authentication information if needed */
193
+ private createAuthInterceptor(): Interceptor {
194
+ return (options, nextCall) => {
195
+ return new InterceptingCall(nextCall(options), {
196
+ start: (metadata, listener, next) => {
197
+ if (this.authInformation?.jwtToken !== undefined) {
198
+ metadata.set('authorization', 'Bearer ' + this.authInformation.jwtToken);
199
+ this.refreshAuthInformationIfNeeded();
200
+ next(metadata, listener);
201
+ } else {
202
+ next(metadata, listener);
203
+ }
204
+ }
205
+ });
206
+ };
207
+ }
208
+
209
+ createTx(ops: PlCallOps = {}): LLPlTransaction {
210
+ return new LLPlTransaction((abortSignal) => {
211
+ let totalAbortSignal = abortSignal;
212
+ if (ops.abortSignal)
213
+ // this will be fixed in typescript 5.5.0
214
+ // see this https://github.com/microsoft/TypeScript/issues/58026
215
+ // and this https://github.com/microsoft/TypeScript/pull/58211
216
+ totalAbortSignal = (AbortSignal as any).any([totalAbortSignal, ops.abortSignal]);
217
+ return this.grpcPl.tx({
218
+ abort: totalAbortSignal,
219
+ timeout: ops.timeout ?? this.conf.defaultTransactionTimeout
220
+ });
221
+ });
222
+ }
223
+
224
+ /** Closes underlying transport */
225
+ public close() {
226
+ this.grpcTransport.close();
227
+ }
228
+ }
@@ -0,0 +1,152 @@
1
+ import { getTestLLClient } from '../test/test_config';
2
+ import { TxAPI_Open_Request_WritableTx } from '../proto/github.com/milaboratory/pl/plapi/plapiproto/api';
3
+ import { createLocalResourceId } from './types';
4
+
5
+ import { isTimeoutOrCancelError } from './errors';
6
+ import { Aborted } from '@milaboratories/ts-helpers';
7
+
8
+ test('transaction timeout test', async () => {
9
+ const client = await getTestLLClient();
10
+ const tx = client.createTx({ timeout: 500 });
11
+
12
+ await expect(async () => {
13
+ const response = await tx.send(
14
+ {
15
+ oneofKind: 'txOpen',
16
+ txOpen: { name: 'test', writable: TxAPI_Open_Request_WritableTx.WRITABLE }
17
+ },
18
+ false
19
+ );
20
+ expect(response.txOpen.tx?.isValid).toBeTruthy();
21
+ await tx.await();
22
+ }).rejects.toThrow(Aborted);
23
+ });
24
+
25
+ test('check timeout error type (passive)', async () => {
26
+ const client = await getTestLLClient();
27
+ const tx = client.createTx({ timeout: 500 });
28
+
29
+ try {
30
+ const response = await tx.send(
31
+ {
32
+ oneofKind: 'txOpen',
33
+ txOpen: { name: 'test', writable: TxAPI_Open_Request_WritableTx.WRITABLE }
34
+ },
35
+ false
36
+ );
37
+ expect(response.txOpen.tx?.isValid).toBeTruthy();
38
+ await tx.await();
39
+ } catch (err: unknown) {
40
+ expect(isTimeoutOrCancelError(err)).toBe(true);
41
+ }
42
+ });
43
+
44
+ test('check timeout error type (active)', async () => {
45
+ const client = await getTestLLClient();
46
+ const tx = client.createTx({ timeout: 500 });
47
+
48
+ try {
49
+ const openResponse = await tx.send(
50
+ {
51
+ oneofKind: 'txOpen',
52
+ txOpen: { name: 'test', writable: TxAPI_Open_Request_WritableTx.WRITABLE }
53
+ },
54
+ false
55
+ );
56
+ expect(openResponse.txOpen.tx?.isValid).toBeTruthy();
57
+
58
+ const rData = Uint8Array.from([
59
+ (Math.random() * 256) & 0xff,
60
+ (Math.random() * 256) & 0xff,
61
+ (Math.random() * 256) & 0xff,
62
+ (Math.random() * 256) & 0xff,
63
+ (Math.random() * 256) & 0xff,
64
+ (Math.random() * 256) & 0xff,
65
+ (Math.random() * 256) & 0xff,
66
+ (Math.random() * 256) & 0xff
67
+ ]);
68
+
69
+ const createResponse = await tx.send(
70
+ {
71
+ oneofKind: 'resourceCreateValue',
72
+ resourceCreateValue: {
73
+ id: createLocalResourceId(false, 1, 1),
74
+ type: { name: 'TestValue', version: '1' },
75
+ data: rData,
76
+ errorIfExists: false
77
+ }
78
+ },
79
+ false
80
+ );
81
+ const id = (await createResponse).resourceCreateValue.resourceId;
82
+
83
+ while (true) {
84
+ const vr = await tx.send(
85
+ {
86
+ oneofKind: 'resourceGet',
87
+ resourceGet: { resourceId: id, loadFields: false }
88
+ },
89
+ false
90
+ );
91
+
92
+ expect(Buffer.compare(vr.resourceGet.resource!.data, rData)).toBe(0);
93
+ }
94
+ } catch (err: unknown) {
95
+ expect(isTimeoutOrCancelError(err)).toBe(true);
96
+ }
97
+ });
98
+
99
+ test('check is abort error (active)', async () => {
100
+ const client = await getTestLLClient();
101
+ const tx = client.createTx({ abortSignal: AbortSignal.timeout(100) });
102
+
103
+ try {
104
+ const openResponse = await tx.send(
105
+ {
106
+ oneofKind: 'txOpen',
107
+ txOpen: { name: 'test', writable: TxAPI_Open_Request_WritableTx.WRITABLE }
108
+ },
109
+ false
110
+ );
111
+ expect(openResponse.txOpen.tx?.isValid).toBeTruthy();
112
+
113
+ const rData = Uint8Array.from([
114
+ Math.random() & 0xff,
115
+ Math.random() & 0xff,
116
+ Math.random() & 0xff,
117
+ Math.random() & 0xff,
118
+ Math.random() & 0xff,
119
+ Math.random() & 0xff,
120
+ Math.random() & 0xff,
121
+ Math.random() & 0xff
122
+ ]);
123
+
124
+ const createResponse = await tx.send(
125
+ {
126
+ oneofKind: 'resourceCreateValue',
127
+ resourceCreateValue: {
128
+ id: createLocalResourceId(false, 1, 1),
129
+ type: { name: 'TestValue', version: '1' },
130
+ data: rData,
131
+ errorIfExists: false
132
+ }
133
+ },
134
+ false
135
+ );
136
+ const id = (await createResponse).resourceCreateValue.resourceId;
137
+
138
+ while (true) {
139
+ const vr = await tx.send(
140
+ {
141
+ oneofKind: 'resourceGet',
142
+ resourceGet: { resourceId: id, loadFields: false }
143
+ },
144
+ false
145
+ );
146
+
147
+ expect(Buffer.compare(vr.resourceGet.resource!.data, rData)).toBe(0);
148
+ }
149
+ } catch (err: unknown) {
150
+ expect(isTimeoutOrCancelError(err)).toBe(true);
151
+ }
152
+ });