@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.
- package/README.md +52 -0
- package/dist/index.cjs +14527 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +14426 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/core/auth.ts +27 -0
- package/src/core/client.test.ts +47 -0
- package/src/core/client.ts +302 -0
- package/src/core/config.test.ts +19 -0
- package/src/core/config.ts +197 -0
- package/src/core/default_client.ts +161 -0
- package/src/core/driver.ts +30 -0
- package/src/core/error.test.ts +14 -0
- package/src/core/errors.ts +84 -0
- package/src/core/http.ts +178 -0
- package/src/core/ll_client.test.ts +111 -0
- package/src/core/ll_client.ts +228 -0
- package/src/core/ll_transaction.test.ts +152 -0
- package/src/core/ll_transaction.ts +333 -0
- package/src/core/transaction.test.ts +173 -0
- package/src/core/transaction.ts +730 -0
- package/src/core/type_conversion.ts +121 -0
- package/src/core/types.test.ts +22 -0
- package/src/core/types.ts +223 -0
- package/src/core/unauth_client.test.ts +21 -0
- package/src/core/unauth_client.ts +48 -0
- package/src/helpers/pl.ts +141 -0
- package/src/helpers/poll.ts +178 -0
- package/src/helpers/rich_resource_types.test.ts +22 -0
- package/src/helpers/rich_resource_types.ts +84 -0
- package/src/helpers/smart_accessors.ts +146 -0
- package/src/helpers/state_helpers.ts +5 -0
- package/src/helpers/tx_helpers.ts +24 -0
- package/src/index.ts +14 -0
- package/src/proto/github.com/googleapis/googleapis/google/rpc/status.ts +125 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.ts +45 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.ts +271 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.client.ts +51 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.ts +380 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.ts +59 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.ts +450 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.ts +148 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.ts +706 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +406 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api.ts +12636 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api_types.ts +1384 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/base_types.ts +181 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/import.ts +251 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/resource_types.ts +693 -0
- package/src/proto/google/api/http.ts +687 -0
- package/src/proto/google/protobuf/any.ts +326 -0
- package/src/proto/google/protobuf/descriptor.ts +4502 -0
- package/src/proto/google/protobuf/duration.ts +230 -0
- package/src/proto/google/protobuf/empty.ts +81 -0
- package/src/proto/google/protobuf/struct.ts +482 -0
- package/src/proto/google/protobuf/timestamp.ts +287 -0
- package/src/proto/google/protobuf/wrappers.ts +751 -0
- package/src/test/test_config.test.ts +6 -0
- package/src/test/test_config.ts +166 -0
- package/src/util/branding.ts +4 -0
- package/src/util/pl.ts +11 -0
- package/src/util/util.test.ts +10 -0
- 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
|
+
});
|