@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
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@milaboratories/pl-client",
3
+ "version": "2.4.10",
4
+ "description": "New TS/JS client for Platform API",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.cjs"
11
+ }
12
+ },
13
+ "files": [
14
+ "./dist/**/*",
15
+ "./src/**/*"
16
+ ],
17
+ "dependencies": {
18
+ "@grpc/grpc-js": "^1.11.2",
19
+ "@protobuf-ts/grpc-transport": "^2.9.4",
20
+ "@protobuf-ts/runtime": "^2.9.4",
21
+ "@protobuf-ts/runtime-rpc": "^2.9.4",
22
+ "canonicalize": "^2.0.0",
23
+ "denque": "^2.1.0",
24
+ "https-proxy-agent": "^7.0.5",
25
+ "long": "^5.2.3",
26
+ "undici": "^6.19.8",
27
+ "utility-types": "^3.11.0",
28
+ "yaml": "^2.5.1",
29
+ "@milaboratories/ts-helpers": "^1.0.22"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.6.2",
33
+ "tsup": "~8.2.4",
34
+ "@protobuf-ts/plugin": "^2.9.4",
35
+ "@types/http-proxy": "^1.17.15",
36
+ "@types/jest": "^29.5.13",
37
+ "jest": "^29.7.0",
38
+ "ts-jest": "^29.2.5",
39
+ "prettier": "^3.3.3",
40
+ "@milaboratories/platforma-build-configs": "1.0.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "build-debug": "tsc",
45
+ "build-watch": "tsup --watch",
46
+ "type-check": "tsc --noEmit --composite false",
47
+ "test": "jest --runInBand"
48
+ }
49
+ }
@@ -0,0 +1,27 @@
1
+ import { AuthInformation } from './config';
2
+ import { parsePlJwt } from '../util/pl';
3
+
4
+ /** Returns a timestamp when current authorization information should be refreshed.
5
+ * Compare the value with Date.now(). */
6
+ export function inferAuthRefreshTime(
7
+ info: AuthInformation,
8
+ maxRefreshSeconds: number
9
+ ): number | undefined {
10
+ if (info.jwtToken === undefined) return undefined;
11
+
12
+ const { exp, iat } = parsePlJwt(info.jwtToken);
13
+
14
+ return (
15
+ Math.min(
16
+ // in the middle between issue and expiration time points
17
+ (iat + exp) / 2,
18
+ iat + maxRefreshSeconds
19
+ ) * 1000
20
+ );
21
+ }
22
+
23
+ export function expirationFromAuthInformation(authInfo: AuthInformation): number | undefined {
24
+ if (authInfo.jwtToken === undefined) return undefined;
25
+ const parsed = parsePlJwt(authInfo.jwtToken);
26
+ return parsed.exp * 1000;
27
+ }
@@ -0,0 +1,47 @@
1
+ import { getTestClient, getTestClientConf } from '../test/test_config';
2
+ import { PlClient } from './client';
3
+ import { PlDriver, PlDriverDefinition } from './driver';
4
+ import { GrpcTransport } from '@protobuf-ts/grpc-transport';
5
+ import { Dispatcher, request } from 'undici';
6
+
7
+ test('test client init', async () => {
8
+ const client = await getTestClient(undefined);
9
+ });
10
+
11
+ test('test client alternative root init', async () => {
12
+ const aRootName = 'test_root';
13
+ const { conf, auth } = await getTestClientConf();
14
+ const clientA = await PlClient.init({ ...conf, alternativeRoot: aRootName }, auth);
15
+ const clientB = await PlClient.init(conf, auth);
16
+ const result = await clientB.deleteAlternativeRoot(aRootName);
17
+ expect(result).toBe(true);
18
+ });
19
+
20
+ test('test client init', async () => {
21
+ const client = await getTestClient();
22
+ });
23
+
24
+ interface SimpleDriver extends PlDriver {
25
+ ping(): Promise<string>;
26
+ }
27
+
28
+ const SimpleDriverDefinition: PlDriverDefinition<SimpleDriver> = {
29
+ name: 'SimpleDriver',
30
+ init(pl: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher): SimpleDriver {
31
+ return {
32
+ async ping(): Promise<string> {
33
+ const response = await request('https://cdn.milaboratory.com/ping', {
34
+ dispatcher: httpDispatcher
35
+ });
36
+ return await response.body.text();
37
+ },
38
+ close() {}
39
+ };
40
+ }
41
+ };
42
+
43
+ test('test driver', async () => {
44
+ const client = await getTestClient();
45
+ const drv = client.getDriver(SimpleDriverDefinition);
46
+ expect(await drv.ping()).toEqual('pong');
47
+ });
@@ -0,0 +1,302 @@
1
+ import { AuthOps, PlClientConfig, PlConnectionStatusListener } from './config';
2
+ import { LLPlClient, PlCallOps } from './ll_client';
3
+ import { AnyResourceRef, PlTransaction, toGlobalResourceId, TxCommitConflict } from './transaction';
4
+ import { createHash } from 'crypto';
5
+ import {
6
+ ensureResourceIdNotNull,
7
+ isNullResourceId,
8
+ NullResourceId,
9
+ OptionalResourceId,
10
+ ResourceId
11
+ } from './types';
12
+ import { ClientRoot } from '../helpers/pl';
13
+ import {
14
+ assertNever,
15
+ createRetryState,
16
+ nextRetryStateOrError,
17
+ RetryOptions
18
+ } from '@milaboratories/ts-helpers';
19
+ import { PlDriver, PlDriverDefinition } from './driver';
20
+ import { MaintenanceAPI_Ping_Response } from '../proto/github.com/milaboratory/pl/plapi/plapiproto/api';
21
+ import * as tp from 'node:timers/promises';
22
+ import { Dispatcher } from 'undici';
23
+
24
+ export type TxOps = PlCallOps & {
25
+ sync?: boolean;
26
+ retryOptions?: RetryOptions;
27
+ };
28
+
29
+ const defaultTxOps = {
30
+ sync: false
31
+ };
32
+
33
+ const AnonymousClientRoot = 'AnonymousRoot';
34
+
35
+ function alternativeRootFieldName(alternativeRoot: string): string {
36
+ return `alternative_root_${alternativeRoot}`;
37
+ }
38
+
39
+ /** Client to access core PL API. */
40
+ export class PlClient {
41
+ private readonly ll: LLPlClient;
42
+ private readonly drivers = new Map<String, PlDriver>();
43
+
44
+ /** Artificial delay introduced after write transactions completion, to
45
+ * somewhat throttle the load on pl. Delay introduced after sync, if requested. */
46
+ private readonly txDelay: number;
47
+
48
+ /** Last resort measure to solve complicated race conditions in pl. */
49
+ private readonly forceSync: boolean;
50
+
51
+ /** Last resort measure to solve complicated race conditions in pl. */
52
+ private readonly defaultRetryOptions: RetryOptions;
53
+
54
+ /** Stores client root (this abstraction is intended for future implementation of the security model)*/
55
+ private _clientRoot: OptionalResourceId = NullResourceId;
56
+
57
+ private _serverInfo: MaintenanceAPI_Ping_Response | undefined = undefined;
58
+
59
+ private constructor(
60
+ configOrAddress: PlClientConfig | string,
61
+ auth: AuthOps,
62
+ ops: {
63
+ statusListener?: PlConnectionStatusListener;
64
+ } = {}
65
+ ) {
66
+ this.ll = new LLPlClient(configOrAddress, { auth, ...ops });
67
+ const conf = this.ll.conf;
68
+ this.txDelay = conf.txDelay;
69
+ this.forceSync = conf.forceSync;
70
+ switch (conf.retryBackoffAlgorithm) {
71
+ case 'exponential':
72
+ this.defaultRetryOptions = {
73
+ type: 'exponentialBackoff',
74
+ initialDelay: conf.retryInitialDelay,
75
+ maxAttempts: conf.retryMaxAttempts,
76
+ backoffMultiplier: conf.retryExponentialBackoffMultiplier,
77
+ jitter: conf.retryJitter
78
+ };
79
+ break;
80
+ case 'linear':
81
+ this.defaultRetryOptions = {
82
+ type: 'linearBackoff',
83
+ initialDelay: conf.retryInitialDelay,
84
+ maxAttempts: conf.retryMaxAttempts,
85
+ backoffStep: conf.retryLinearBackoffStep,
86
+ jitter: conf.retryJitter
87
+ };
88
+ break;
89
+ default:
90
+ assertNever(conf.retryBackoffAlgorithm);
91
+ }
92
+ }
93
+
94
+ public async ping(): Promise<MaintenanceAPI_Ping_Response> {
95
+ return (await this.ll.grpcPl.ping({})).response;
96
+ }
97
+
98
+ public get conf(): PlClientConfig {
99
+ return this.ll.conf;
100
+ }
101
+
102
+ public get httpDispatcher(): Dispatcher {
103
+ return this.ll.httpDispatcher;
104
+ }
105
+
106
+ private get initialized() {
107
+ return !isNullResourceId(this._clientRoot);
108
+ }
109
+
110
+ private checkInitialized() {
111
+ if (!this.initialized) throw new Error('Client not initialized');
112
+ }
113
+
114
+ public get clientRoot(): ResourceId {
115
+ this.checkInitialized();
116
+ return ensureResourceIdNotNull(this._clientRoot);
117
+ }
118
+
119
+ public get serverInfo(): MaintenanceAPI_Ping_Response {
120
+ this.checkInitialized();
121
+ return this._serverInfo!;
122
+ }
123
+
124
+ /** Currently implements custom logic to emulate future behaviour with single root. */
125
+ public async init() {
126
+ if (this.initialized) throw new Error('Already initialized');
127
+
128
+ // calculating reproducible root name from the username
129
+ const user = this.ll.authUser;
130
+ const mainRootName =
131
+ user === null ? AnonymousClientRoot : createHash('sha256').update(user).digest('hex');
132
+
133
+ this._serverInfo = await this.ping();
134
+
135
+ this._clientRoot = await this._withTx('initialization', true, NullResourceId, async (tx) => {
136
+ let mainRoot: AnyResourceRef;
137
+
138
+ if (await tx.checkResourceNameExists(mainRootName))
139
+ mainRoot = await tx.getResourceByName(mainRootName);
140
+ else {
141
+ mainRoot = tx.createRoot(ClientRoot);
142
+ tx.setResourceName(mainRootName, mainRoot);
143
+ }
144
+
145
+ if (this.conf.alternativeRoot === undefined) {
146
+ await tx.commit();
147
+ return await toGlobalResourceId(mainRoot);
148
+ } else {
149
+ const aFId = {
150
+ resourceId: mainRoot,
151
+ fieldName: alternativeRootFieldName(this.conf.alternativeRoot)
152
+ };
153
+
154
+ const altRoot = tx.createEphemeral(ClientRoot);
155
+ tx.lock(altRoot);
156
+ tx.createField(aFId, 'Dynamic');
157
+ tx.setField(aFId, altRoot);
158
+ await tx.commit();
159
+
160
+ return await altRoot.globalId;
161
+ }
162
+ });
163
+
164
+ // try {
165
+ //
166
+ // } catch (error: unknown) {
167
+ // if(isUnauthenticated(error) && this.)
168
+ // }
169
+ }
170
+
171
+ /** Returns true if field existed */
172
+ public async deleteAlternativeRoot(alternativeRootName: string): Promise<boolean> {
173
+ this.checkInitialized();
174
+ if (this.ll.conf.alternativeRoot !== undefined)
175
+ throw new Error('Initialized with alternative root.');
176
+ return await this.withWriteTx('delete-alternative-root', async (tx) => {
177
+ const fId = {
178
+ resourceId: tx.clientRoot,
179
+ fieldName: alternativeRootFieldName(alternativeRootName)
180
+ };
181
+ const exists = tx.fieldExists(fId);
182
+ tx.removeField(fId);
183
+ await tx.commit();
184
+ return await exists;
185
+ });
186
+ }
187
+
188
+ private async _withTx<T>(
189
+ name: string,
190
+ writable: boolean,
191
+ clientRoot: OptionalResourceId,
192
+ body: (tx: PlTransaction) => Promise<T>,
193
+ ops?: TxOps
194
+ ): Promise<T> {
195
+ // for exponential / linear backoff
196
+ let retryState = createRetryState(ops?.retryOptions ?? this.defaultRetryOptions);
197
+
198
+ while (true) {
199
+ // opening low-level tx
200
+ const llTx = this.ll.createTx(ops);
201
+ // wrapping it into high-level tx (this also asynchronously sends initialization message)
202
+ const tx = new PlTransaction(llTx, name, writable, clientRoot);
203
+
204
+ let ok = false;
205
+ let result: T | undefined = undefined;
206
+ let txId;
207
+
208
+ try {
209
+ // executing transaction body
210
+ result = await body(tx);
211
+ ok = true;
212
+ } catch (e: unknown) {
213
+ // the only recoverable
214
+ if (e instanceof TxCommitConflict) {
215
+ // ignoring
216
+ // TODO collect stats
217
+ } else {
218
+ throw e;
219
+ }
220
+ } finally {
221
+ // close underlying grpc stream, if not yet done
222
+
223
+ // even though we can skip two lines below for read-only transactions,
224
+ // we don't do it to simplify reasoning about what is going on in
225
+ // concurrent code, especially in significant latency situations
226
+ await tx.complete();
227
+ await tx.await();
228
+
229
+ txId = await tx.getGlobalTxId();
230
+ }
231
+
232
+ if (ok) {
233
+ // syncing on transaction if requested
234
+ if (ops?.sync === undefined ? this.forceSync : ops?.sync)
235
+ await this.ll.grpcPl.txSync({ txId });
236
+
237
+ // introducing artificial delay, if requested
238
+ if (writable && this.txDelay > 0)
239
+ await tp.setTimeout(this.txDelay, undefined, { signal: ops?.abortSignal });
240
+
241
+ return result!;
242
+ }
243
+
244
+ // we only get here after TxCommitConflict error,
245
+ // all other errors terminate this loop instantly
246
+
247
+ await tp.setTimeout(retryState.nextDelay, undefined, { signal: ops?.abortSignal });
248
+ retryState = nextRetryStateOrError(retryState);
249
+ }
250
+ }
251
+
252
+ private async withTx<T>(
253
+ name: string,
254
+ writable: boolean,
255
+ body: (tx: PlTransaction) => Promise<T>,
256
+ ops: Partial<TxOps> = {}
257
+ ): Promise<T> {
258
+ this.checkInitialized();
259
+ return await this._withTx(name, writable, this.clientRoot, body, { ...ops, ...defaultTxOps });
260
+ }
261
+
262
+ public async withWriteTx<T>(
263
+ name: string,
264
+ body: (tx: PlTransaction) => Promise<T>,
265
+ ops: Partial<TxOps> = {}
266
+ ): Promise<T> {
267
+ return await this.withTx(name, true, body, { ...ops, ...defaultTxOps });
268
+ }
269
+
270
+ public async withReadTx<T>(
271
+ name: string,
272
+ body: (tx: PlTransaction) => Promise<T>,
273
+ ops: Partial<TxOps> = {}
274
+ ): Promise<T> {
275
+ return await this.withTx(name, false, body, { ...ops, ...defaultTxOps });
276
+ }
277
+
278
+ public getDriver<Drv extends PlDriver>(definition: PlDriverDefinition<Drv>): Drv {
279
+ const attached = this.drivers.get(definition.name);
280
+ if (attached !== undefined) return attached as Drv;
281
+ const driver = definition.init(this, this.ll.grpcTransport, this.ll.httpDispatcher);
282
+ this.drivers.set(definition.name, driver);
283
+ return driver;
284
+ }
285
+
286
+ /** Closes underlying transport */
287
+ public close() {
288
+ this.ll.close();
289
+ }
290
+
291
+ public static async init(
292
+ configOrAddress: PlClientConfig | string,
293
+ auth: AuthOps,
294
+ ops: {
295
+ statusListener?: PlConnectionStatusListener;
296
+ } = {}
297
+ ) {
298
+ const pl = new PlClient(configOrAddress, auth, ops);
299
+ await pl.init();
300
+ return pl;
301
+ }
302
+ }
@@ -0,0 +1,19 @@
1
+ import { plAddressToConfig } from './config';
2
+
3
+ test('config form url no auth', () => {
4
+ const conf = plAddressToConfig('http://127.0.0.1:6345');
5
+ expect(conf.user).toBeUndefined();
6
+ expect(conf.password).toBeUndefined();
7
+ });
8
+
9
+ test('config form url with auth', () => {
10
+ const conf = plAddressToConfig('http://user1:password2@127.0.0.1:6345');
11
+ expect(conf.user).toEqual('user1');
12
+ expect(conf.password).toEqual('password2');
13
+ });
14
+
15
+ test('config form url with auth and special symbols', () => {
16
+ const conf = plAddressToConfig('http://user1:password232$@127.0.0.1:6345');
17
+ expect(conf.user).toEqual('user1');
18
+ expect(conf.password).toEqual('password232$');
19
+ });
@@ -0,0 +1,197 @@
1
+ /** Base configuration structure for PL client */
2
+ export interface PlClientConfig {
3
+ /** Port and host of remote pl server */
4
+ hostAndPort: string;
5
+
6
+ /** If set, client will expose a nested object under a field with name `alternative_root_${alternativeRoot}` as a
7
+ * client root. */
8
+ alternativeRoot?: string;
9
+
10
+ /** If true, client will establish tls connection to the server, using default
11
+ * CA of node instance. */
12
+ // Not implementing custom ssl validation logic for now.
13
+ // Implementing it in a correct way is really nontrivial thing,
14
+ // real use-cases should be considered.
15
+ ssl: boolean;
16
+
17
+ /** Default timeout in milliseconds for unary calls, like ping and login. */
18
+ defaultRequestTimeout: number;
19
+ /** Default timeout in milliseconds for transaction, should be adjusted for
20
+ * long round-trip connections. */
21
+ defaultTransactionTimeout: number;
22
+
23
+ /** Controls what TTL will be requested from the server, when new JWT token
24
+ * is requested. */
25
+ authTTLSeconds: number;
26
+ /** If token is older than this time, it will be refreshed regardless of its
27
+ * expiration time. */
28
+ authMaxRefreshSeconds: number;
29
+
30
+ /** Proxy server URL to use for pl connection. */
31
+ grpcProxy?: string;
32
+ /** Proxy server URL to use for http connections of pl drivers, like file
33
+ * downloading. */
34
+ httpProxy?: string;
35
+
36
+ /** Username extracted from pl URL. Ignored by {@link PlClient}, picked up by {@link defaultPlClient}. */
37
+ user?: string;
38
+ /** Password extracted from pl URL. Ignored by {@link PlClient}, picked up by {@link defaultPlClient}. */
39
+ password?: string;
40
+
41
+ /** Artificial delay introduced after write transactions completion, to
42
+ * somewhat throttle the load on pl. Delay introduced after sync, if requested. */
43
+ txDelay: number;
44
+
45
+ /** Last resort measure to solve complicated race conditions in pl. */
46
+ forceSync: boolean;
47
+
48
+ //
49
+ // Retry
50
+ //
51
+
52
+ /**
53
+ * What type of backoff strategy to use in transaction retries
54
+ * (pl uses optimistic transaction model with regular retries in write transactions)
55
+ * */
56
+ retryBackoffAlgorithm: 'exponential' | 'linear';
57
+ /** Maximal number of attempts in */
58
+ retryMaxAttempts: number;
59
+ /** Delay after first failed attempt, in ms. */
60
+ retryInitialDelay: number;
61
+ /** Each time delay will be multiplied by this number (1.5 means plus on 50% each attempt) */
62
+ retryExponentialBackoffMultiplier: number;
63
+ /** [used only for ] This value will be added to the delay from the previous step, in ms */
64
+ retryLinearBackoffStep: number;
65
+ /** Value from 0 to 1, determine level of randomness to introduce to the backoff delays sequence. (0 meaning no randomness) */
66
+ retryJitter: number;
67
+ }
68
+
69
+ export const DEFAULT_REQUEST_TIMEOUT = 1000;
70
+ export const DEFAULT_TX_TIMEOUT = 10_000;
71
+ export const DEFAULT_TOKEN_TTL_SECONDS = 31 * 24 * 60 * 60;
72
+ export const DEFAULT_AUTH_MAX_REFRESH = 12 * 24 * 60 * 60;
73
+
74
+ export const DEFAULT_RETRY_BACKOFF_ALGORITHM = 'exponential';
75
+ export const DEFAULT_RETRY_MAX_ATTEMPTS = 10;
76
+ export const DEFAULT_RETRY_INITIAL_DELAY = 4; // 4 ms
77
+ export const DEFAULT_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER = 2; // + 100% on each round
78
+ export const DEFAULT_RETRY_LINEAR_BACKOFF_STEP = 50; // + 50 ms
79
+ export const DEFAULT_RETRY_JITTER = 0.3; // 30%
80
+
81
+ type PlConfigOverrides = Partial<
82
+ Pick<
83
+ PlClientConfig,
84
+ 'ssl' | 'defaultRequestTimeout' | 'defaultTransactionTimeout' | 'httpProxy' | 'grpcProxy'
85
+ >
86
+ >;
87
+
88
+ function parseInt(s: string | null | undefined): number | undefined {
89
+ if (!s) return undefined;
90
+ const num = Number(s);
91
+ if (num === Number.NaN) throw new Error(`Can't parse number: ${s}`);
92
+ return num;
93
+ }
94
+
95
+ /** Parses pl url and creates a config object that can be passed to
96
+ * {@link PlClient} of {@link UnauthenticatedPlClient}. */
97
+ export function plAddressToConfig(
98
+ address: string,
99
+ overrides: PlConfigOverrides = {}
100
+ ): PlClientConfig {
101
+ if (address.indexOf('://') === -1)
102
+ // non-url address
103
+ return {
104
+ hostAndPort: address,
105
+ ssl: false,
106
+ defaultRequestTimeout: DEFAULT_REQUEST_TIMEOUT,
107
+ defaultTransactionTimeout: DEFAULT_TX_TIMEOUT,
108
+ authTTLSeconds: DEFAULT_TOKEN_TTL_SECONDS,
109
+ authMaxRefreshSeconds: DEFAULT_AUTH_MAX_REFRESH,
110
+ txDelay: 0,
111
+ forceSync: false,
112
+
113
+ retryBackoffAlgorithm: DEFAULT_RETRY_BACKOFF_ALGORITHM,
114
+ retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
115
+ retryInitialDelay: DEFAULT_RETRY_INITIAL_DELAY,
116
+ retryExponentialBackoffMultiplier: DEFAULT_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER,
117
+ retryLinearBackoffStep: DEFAULT_RETRY_LINEAR_BACKOFF_STEP,
118
+ retryJitter: DEFAULT_RETRY_JITTER,
119
+
120
+ ...overrides
121
+ };
122
+
123
+ const url = new URL(address);
124
+
125
+ if (
126
+ url.protocol !== 'https:' &&
127
+ url.protocol !== 'http:' &&
128
+ url.protocol !== 'grpc:' &&
129
+ url.protocol !== 'tls:'
130
+ )
131
+ throw new Error(`Unexpected URL schema: ${url.protocol}`);
132
+
133
+ if (url.pathname !== '/' && url.pathname !== '')
134
+ throw new Error(`Unexpected URL path: ${url.pathname}`);
135
+
136
+ return {
137
+ hostAndPort: url.host, // this also includes port
138
+ alternativeRoot: url.searchParams.get('alternative-root') ?? undefined,
139
+ ssl: url.protocol === 'https:' || url.protocol === 'tls:',
140
+ defaultRequestTimeout:
141
+ parseInt(url.searchParams.get('request-timeout')) ?? DEFAULT_REQUEST_TIMEOUT,
142
+ defaultTransactionTimeout: parseInt(url.searchParams.get('tx-timeout')) ?? DEFAULT_TX_TIMEOUT,
143
+ authTTLSeconds: DEFAULT_TOKEN_TTL_SECONDS,
144
+ authMaxRefreshSeconds: DEFAULT_AUTH_MAX_REFRESH,
145
+ grpcProxy: url.searchParams.get('grpc-proxy') ?? undefined,
146
+ httpProxy: url.searchParams.get('http-proxy') ?? undefined,
147
+ user: url.username === '' ? undefined : url.username,
148
+ password: url.password === '' ? undefined : url.password,
149
+ txDelay: parseInt(url.searchParams.get('tx-delay')) ?? 0,
150
+ forceSync: Boolean(url.searchParams.get('force-sync')) ?? false,
151
+
152
+ retryBackoffAlgorithm: (url.searchParams.get('retry-backoff-algorithm') ??
153
+ DEFAULT_RETRY_BACKOFF_ALGORITHM) as any,
154
+ retryMaxAttempts:
155
+ parseInt(url.searchParams.get('retry-max-attempts')) ?? DEFAULT_RETRY_MAX_ATTEMPTS,
156
+ retryInitialDelay:
157
+ parseInt(url.searchParams.get('retry-initial-delay')) ?? DEFAULT_RETRY_INITIAL_DELAY,
158
+ retryExponentialBackoffMultiplier:
159
+ parseInt(url.searchParams.get('retry-exp-backoff-multiplier')) ??
160
+ DEFAULT_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER,
161
+ retryLinearBackoffStep:
162
+ parseInt(url.searchParams.get('retry-linear-backoff-step')) ??
163
+ DEFAULT_RETRY_LINEAR_BACKOFF_STEP,
164
+ retryJitter: parseInt(url.searchParams.get('retry-backoff-jitter')) ?? DEFAULT_RETRY_JITTER,
165
+
166
+ ...overrides
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Authorization data / JWT Token.
172
+ * Absent JWT Token tells the client to connect as anonymous user.
173
+ * */
174
+ export interface AuthInformation {
175
+ /** Absent token means anonymous access */
176
+ jwtToken?: string;
177
+ }
178
+
179
+ export const AnonymousAuthInformation: AuthInformation = {};
180
+
181
+ /** Authorization related settings to pass to {@link PlClient}. */
182
+ export interface AuthOps {
183
+ /** Initial authorization information */
184
+ authInformation: AuthInformation;
185
+ /** Will be executed after successful authorization information refresh */
186
+ readonly onUpdate?: (newInfo: AuthInformation) => void;
187
+ /** Will be executed if auth-related error happens during normal client operation */
188
+ readonly onAuthError?: () => void;
189
+ /** Will be executed if error encountered during token update */
190
+ readonly onUpdateError?: (error: unknown) => void;
191
+ }
192
+
193
+ /** Connection status. */
194
+ export type PlConnectionStatus = 'OK' | 'Disconnected' | 'Unauthenticated';
195
+
196
+ /** Listener that will be called each time connection status changes. */
197
+ export type PlConnectionStatusListener = (status: PlConnectionStatus) => void;