@temporalio/cloud 1.11.3
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/LICENSE.md +23 -0
- package/README.md +8 -0
- package/lib/cloud-operations-client.d.ts +335 -0
- package/lib/cloud-operations-client.js +392 -0
- package/lib/cloud-operations-client.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -0
- package/lib/pkg.d.ts +5 -0
- package/lib/pkg.js +12 -0
- package/lib/pkg.js.map +1 -0
- package/lib/types.d.ts +3 -0
- package/lib/types.js +29 -0
- package/lib/types.js.map +1 -0
- package/package.json +40 -0
- package/src/cloud-operations-client.ts +600 -0
- package/src/index.ts +7 -0
- package/src/pkg.ts +7 -0
- package/src/types.ts +4 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import * as grpc from '@grpc/grpc-js';
|
|
3
|
+
import type { RPCImpl } from 'protobufjs';
|
|
4
|
+
import {
|
|
5
|
+
filterNullAndUndefined,
|
|
6
|
+
normalizeTlsConfig,
|
|
7
|
+
TLSConfig,
|
|
8
|
+
normalizeGrpcEndpointAddress,
|
|
9
|
+
} from '@temporalio/common/lib/internal-non-workflow';
|
|
10
|
+
import { Duration, msOptionalToNumber } from '@temporalio/common/lib/time';
|
|
11
|
+
import {
|
|
12
|
+
CallContext,
|
|
13
|
+
HealthService,
|
|
14
|
+
Metadata,
|
|
15
|
+
defaultGrpcRetryOptions,
|
|
16
|
+
makeGrpcRetryInterceptor,
|
|
17
|
+
} from '@temporalio/client';
|
|
18
|
+
import pkg from './pkg';
|
|
19
|
+
import { CloudService } from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @experimental
|
|
23
|
+
*/
|
|
24
|
+
export interface CloudOperationsClientOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Connection to use to communicate with the server.
|
|
27
|
+
*/
|
|
28
|
+
connection: CloudOperationsConnection;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Version header for safer mutations.
|
|
32
|
+
* May or may not be required depending on cloud settings.
|
|
33
|
+
*/
|
|
34
|
+
apiVersion?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* High level client for the Temporal Cloud API.
|
|
39
|
+
*
|
|
40
|
+
* @experimental
|
|
41
|
+
*/
|
|
42
|
+
export class CloudOperationsClient {
|
|
43
|
+
/**
|
|
44
|
+
* The underlying {@link CloudOperationsConnection | connection} used by this client.
|
|
45
|
+
*
|
|
46
|
+
* Clients are cheap to create, but connections are expensive. Where that make sense,
|
|
47
|
+
* a single connection may and should be reused by multiple `CloudOperationsClient`.
|
|
48
|
+
*/
|
|
49
|
+
public readonly connection: CloudOperationsConnection;
|
|
50
|
+
public readonly options: Readonly<CloudOperationsClientOptions>;
|
|
51
|
+
|
|
52
|
+
constructor(options: CloudOperationsClientOptions) {
|
|
53
|
+
this.connection = options.connection;
|
|
54
|
+
this.options = filterNullAndUndefined(options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set a deadline for any service requests executed in `fn`'s scope.
|
|
59
|
+
*
|
|
60
|
+
* The deadline is a point in time after which any pending gRPC request will be considered as failed;
|
|
61
|
+
* this will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
|
|
62
|
+
* with code {@link grpc.status.DEADLINE_EXCEEDED|DEADLINE_EXCEEDED}.
|
|
63
|
+
*
|
|
64
|
+
* It is stronly recommended to explicitly set deadlines. If no deadline is set, then it is
|
|
65
|
+
* possible for the client to end up waiting forever for a response.
|
|
66
|
+
*
|
|
67
|
+
* This method is only a convenience wrapper around {@link CloudOperationsConnection.withDeadline}.
|
|
68
|
+
*
|
|
69
|
+
* @param deadline a point in time after which the request will be considered as failed; either a
|
|
70
|
+
* Date object, or a number of milliseconds since the Unix epoch (UTC).
|
|
71
|
+
* @returns the value returned from `fn`
|
|
72
|
+
*
|
|
73
|
+
* @see https://grpc.io/docs/guides/deadlines/
|
|
74
|
+
*/
|
|
75
|
+
public async withDeadline<R>(deadline: number | Date, fn: () => Promise<R>): Promise<R> {
|
|
76
|
+
Date.now;
|
|
77
|
+
return await this.connection.withDeadline(deadline, fn);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set an {@link AbortSignal} that, when aborted, cancels any ongoing service requests executed in
|
|
82
|
+
* `fn`'s scope. This will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
|
|
83
|
+
* with code {@link grpc.status.CANCELLED|CANCELLED}.
|
|
84
|
+
*
|
|
85
|
+
* This method is only a convenience wrapper around {@link CloudOperationsConnection.withAbortSignal}.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* const ctrl = new AbortController();
|
|
91
|
+
* setTimeout(() => ctrl.abort(), 10_000);
|
|
92
|
+
* // 👇 throws if incomplete by the timeout.
|
|
93
|
+
* await conn.withAbortSignal(ctrl.signal, () => client.cloudService.getNamespace({ namespace }));
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @returns value returned from `fn`
|
|
97
|
+
*
|
|
98
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
|
|
99
|
+
*/
|
|
100
|
+
async withAbortSignal<R>(abortSignal: AbortSignal, fn: () => Promise<R>): Promise<R> {
|
|
101
|
+
return await this.connection.withAbortSignal(abortSignal, fn);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Set metadata for any service requests executed in `fn`'s scope.
|
|
106
|
+
*
|
|
107
|
+
* @returns returned value of `fn`
|
|
108
|
+
*
|
|
109
|
+
* This method is only a convenience wrapper around {@link CloudOperationsConnection.withMetadata}.
|
|
110
|
+
*/
|
|
111
|
+
public async withMetadata<R>(metadata: Metadata, fn: () => Promise<R>): Promise<R> {
|
|
112
|
+
return await this.connection.withMetadata(metadata, fn);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Raw gRPC access to the Temporal Cloud Operations service.
|
|
117
|
+
*
|
|
118
|
+
* **NOTE**: The Temporal Cloud Operations service API Version provided in {@link options} is
|
|
119
|
+
* **not** automatically set on requests made via the raw gRPC service object. If the namespace
|
|
120
|
+
* requires it, you may need to do the following:
|
|
121
|
+
*
|
|
122
|
+
* ```
|
|
123
|
+
* const metadata: Metadata = { ['temporal-cloud-api-version']: apiVersion }
|
|
124
|
+
* const response = await client.withMetadata(metadata, async () => {
|
|
125
|
+
* return client.cloudService.getNamespace({ namespace });
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
get cloudService(): CloudService {
|
|
130
|
+
return this.connection.cloudService;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @experimental
|
|
136
|
+
*/
|
|
137
|
+
export interface CloudOperationsConnectionOptions {
|
|
138
|
+
/**
|
|
139
|
+
* The address of the Temporal Cloud Operations API to connect to, in `hostname:port` format.
|
|
140
|
+
*
|
|
141
|
+
* @default saas-api.tmprl.cloud:443
|
|
142
|
+
*/
|
|
143
|
+
address?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* TLS configuration. TLS is required for connecting to the Temporal Cloud Operations API.
|
|
147
|
+
*
|
|
148
|
+
@default true
|
|
149
|
+
*/
|
|
150
|
+
tls?: Pick<TLSConfig, 'serverNameOverride'> | true;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* GRPC Channel arguments
|
|
154
|
+
*
|
|
155
|
+
* @see option descriptions {@link https://grpc.github.io/grpc/core/group__grpc__arg__keys.html | here}
|
|
156
|
+
*
|
|
157
|
+
* By default the SDK sets the following keepalive arguments:
|
|
158
|
+
*
|
|
159
|
+
* ```
|
|
160
|
+
* grpc.keepalive_permit_without_calls: 1
|
|
161
|
+
* grpc.keepalive_time_ms: 30_000
|
|
162
|
+
* grpc.keepalive_timeout_ms: 15_000
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* To opt-out of keepalive, override these keys with `undefined`.
|
|
166
|
+
*/
|
|
167
|
+
channelArgs?: grpc.ChannelOptions;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* {@link https://grpc.github.io/grpc/node/module-src_client_interceptors.html | gRPC interceptors}
|
|
171
|
+
* which will be applied to every RPC call performed by this connection. By default, an interceptor
|
|
172
|
+
* will be included which automatically retries retryable errors. If you do not wish to perform
|
|
173
|
+
* automatic retries, set this to an empty list (or a list with your own interceptors). If you want
|
|
174
|
+
* to add your own interceptors while keeping the default retry behavior, add this to your list of
|
|
175
|
+
* interceptors: `makeGrpcRetryInterceptor(defaultGrpcRetryOptions())`.
|
|
176
|
+
*
|
|
177
|
+
* See:
|
|
178
|
+
* - {@link makeGrpcRetryInterceptor}
|
|
179
|
+
* - {@link defaultGrpcRetryOptions}
|
|
180
|
+
*/
|
|
181
|
+
interceptors?: grpc.Interceptor[];
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Optional mapping of gRPC metadata (HTTP headers) to send with each request to the server. An
|
|
185
|
+
* `Authorization` header set through `metadata` will be ignored and overriden by the value of the
|
|
186
|
+
* {@link apiKey} option.
|
|
187
|
+
*
|
|
188
|
+
* In order to dynamically set metadata, use {@link CloudOperationsConnection.withMetadata}
|
|
189
|
+
*/
|
|
190
|
+
metadata?: Metadata;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* API key for Temporal. This becomes the "Authorization" HTTP header with "Bearer " prepended.
|
|
194
|
+
*
|
|
195
|
+
* You may provide a static string or a callback. Also see {@link CloudOperationsConnection.withApiKey}
|
|
196
|
+
* or {@link Connection.setApiKey}
|
|
197
|
+
*/
|
|
198
|
+
apiKey: string | (() => string);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Milliseconds to wait until establishing a connection with the server.
|
|
202
|
+
*
|
|
203
|
+
* @format number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}
|
|
204
|
+
* @default 10 seconds
|
|
205
|
+
*/
|
|
206
|
+
connectTimeout?: Duration;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type ResolvedCloudOperationsConnectionOptions = Required<
|
|
210
|
+
Omit<CloudOperationsConnectionOptions, 'tls' | 'connectTimeout'>
|
|
211
|
+
> & {
|
|
212
|
+
connectTimeoutMs: number;
|
|
213
|
+
credentials: grpc.ChannelCredentials;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* - Convert {@link ConnectionOptions.tls} to {@link grpc.ChannelCredentials}
|
|
218
|
+
* - Add the grpc.ssl_target_name_override GRPC {@link ConnectionOptions.channelArgs | channel arg}
|
|
219
|
+
* - Add default port to address if port not specified
|
|
220
|
+
* - Set `Authorization` header based on {@link ConnectionOptions.apiKey}
|
|
221
|
+
*/
|
|
222
|
+
function normalizeGRPCConfig(options: CloudOperationsConnectionOptions): ResolvedCloudOperationsConnectionOptions {
|
|
223
|
+
const {
|
|
224
|
+
address: addressFromConfig,
|
|
225
|
+
tls: tlsFromConfig,
|
|
226
|
+
channelArgs,
|
|
227
|
+
interceptors,
|
|
228
|
+
connectTimeout,
|
|
229
|
+
...rest
|
|
230
|
+
} = options;
|
|
231
|
+
|
|
232
|
+
const address = addressFromConfig ? normalizeGrpcEndpointAddress(addressFromConfig, 443) : 'saas-api.tmprl.cloud:443';
|
|
233
|
+
const tls = normalizeTlsConfig(tlsFromConfig) ?? {};
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
address,
|
|
237
|
+
credentials: grpc.credentials.combineChannelCredentials(grpc.credentials.createSsl()),
|
|
238
|
+
channelArgs: {
|
|
239
|
+
'grpc.keepalive_permit_without_calls': 1,
|
|
240
|
+
'grpc.keepalive_time_ms': 30_000,
|
|
241
|
+
'grpc.keepalive_timeout_ms': 15_000,
|
|
242
|
+
max_receive_message_length: 128 * 1024 * 1024, // 128 MB
|
|
243
|
+
...channelArgs,
|
|
244
|
+
...(tls.serverNameOverride
|
|
245
|
+
? {
|
|
246
|
+
'grpc.ssl_target_name_override': tls.serverNameOverride,
|
|
247
|
+
'grpc.default_authority': tls.serverNameOverride,
|
|
248
|
+
}
|
|
249
|
+
: undefined),
|
|
250
|
+
},
|
|
251
|
+
interceptors: interceptors ?? [makeGrpcRetryInterceptor(defaultGrpcRetryOptions())],
|
|
252
|
+
metadata: {},
|
|
253
|
+
connectTimeoutMs: msOptionalToNumber(connectTimeout) ?? 10_000,
|
|
254
|
+
...filterNullAndUndefined(rest),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface RPCImplOptions {
|
|
259
|
+
serviceName: string;
|
|
260
|
+
client: grpc.Client;
|
|
261
|
+
callContextStorage: AsyncLocalStorage<CallContext>;
|
|
262
|
+
interceptors?: grpc.Interceptor[];
|
|
263
|
+
staticMetadata: Metadata;
|
|
264
|
+
apiKeyFnRef: { fn?: () => string };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface CloudOperationsConnectionCtorOptions {
|
|
268
|
+
readonly options: ResolvedCloudOperationsConnectionOptions;
|
|
269
|
+
readonly client: grpc.Client;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Raw gRPC access to the
|
|
273
|
+
* {@link https://github.com/temporalio/api-cloud/blob/main/temporal/api/cloud/cloudservice/v1/service.proto | Temporal Cloud's Operator Service}.
|
|
274
|
+
*/
|
|
275
|
+
readonly cloudService: CloudService;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Raw gRPC access to the standard gRPC {@link https://github.com/grpc/grpc/blob/92f58c18a8da2728f571138c37760a721c8915a2/doc/health-checking.md | health service}.
|
|
279
|
+
*/
|
|
280
|
+
readonly healthService: HealthService;
|
|
281
|
+
|
|
282
|
+
readonly callContextStorage: AsyncLocalStorage<CallContext>;
|
|
283
|
+
readonly apiKeyFnRef: { fn?: () => string };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Client connection to the Temporal Cloud Operations Service endpoint.
|
|
288
|
+
*
|
|
289
|
+
* ⚠️ Connections are expensive to construct and should be reused.
|
|
290
|
+
* Make sure to {@link close} any unused connections to avoid leaking resources.
|
|
291
|
+
*
|
|
292
|
+
* @experimental
|
|
293
|
+
*/
|
|
294
|
+
export class CloudOperationsConnection {
|
|
295
|
+
private static readonly Client = grpc.makeGenericClientConstructor({}, 'CloudService', {});
|
|
296
|
+
|
|
297
|
+
public readonly options: ResolvedCloudOperationsConnectionOptions;
|
|
298
|
+
private readonly client: grpc.Client;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Used to ensure `ensureConnected` is called once.
|
|
302
|
+
*/
|
|
303
|
+
private connectPromise?: Promise<void>;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Raw gRPC access to the
|
|
307
|
+
* {@link https://github.com/temporalio/api-cloud/blob/main/temporal/api/cloud/cloudservice/v1/service.proto | Temporal Cloud's Operator Service}.
|
|
308
|
+
*
|
|
309
|
+
* The Temporal Cloud Operator Service API defines how Temporal SDKs and other clients interact
|
|
310
|
+
* with the Temporal Cloud platform to perform administrative functions like registering a search
|
|
311
|
+
* attribute or a namespace.
|
|
312
|
+
*
|
|
313
|
+
* This Service API is NOT compatible with self-hosted Temporal deployments.
|
|
314
|
+
*/
|
|
315
|
+
public readonly cloudService: CloudService;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Raw gRPC access to the standard gRPC {@link https://github.com/grpc/grpc/blob/92f58c18a8da2728f571138c37760a721c8915a2/doc/health-checking.md | health service}.
|
|
319
|
+
*/
|
|
320
|
+
public readonly healthService: HealthService;
|
|
321
|
+
|
|
322
|
+
private readonly callContextStorage: AsyncLocalStorage<CallContext>;
|
|
323
|
+
private readonly apiKeyFnRef: { fn?: () => string };
|
|
324
|
+
|
|
325
|
+
protected static createCtorOptions(options: CloudOperationsConnectionOptions): CloudOperationsConnectionCtorOptions {
|
|
326
|
+
const normalizedOptions = normalizeGRPCConfig(options);
|
|
327
|
+
const apiKeyFnRef: { fn?: () => string } = {};
|
|
328
|
+
if (typeof normalizedOptions.apiKey === 'string') {
|
|
329
|
+
const apiKey = normalizedOptions.apiKey;
|
|
330
|
+
apiKeyFnRef.fn = () => apiKey;
|
|
331
|
+
} else {
|
|
332
|
+
apiKeyFnRef.fn = normalizedOptions.apiKey;
|
|
333
|
+
}
|
|
334
|
+
// Allow overriding this
|
|
335
|
+
normalizedOptions.metadata['client-name'] ??= 'temporal-typescript';
|
|
336
|
+
normalizedOptions.metadata['client-version'] ??= pkg.version;
|
|
337
|
+
|
|
338
|
+
const client = new this.Client(
|
|
339
|
+
normalizedOptions.address,
|
|
340
|
+
normalizedOptions.credentials,
|
|
341
|
+
normalizedOptions.channelArgs
|
|
342
|
+
);
|
|
343
|
+
const callContextStorage = new AsyncLocalStorage<CallContext>();
|
|
344
|
+
|
|
345
|
+
const cloudRpcImpl = this.generateRPCImplementation({
|
|
346
|
+
serviceName: 'temporal.api.cloud.cloudservice.v1.CloudService',
|
|
347
|
+
client,
|
|
348
|
+
callContextStorage,
|
|
349
|
+
interceptors: normalizedOptions?.interceptors,
|
|
350
|
+
staticMetadata: normalizedOptions.metadata,
|
|
351
|
+
apiKeyFnRef,
|
|
352
|
+
});
|
|
353
|
+
const cloudService = CloudService.create(cloudRpcImpl, false, false);
|
|
354
|
+
|
|
355
|
+
const healthRpcImpl = this.generateRPCImplementation({
|
|
356
|
+
serviceName: 'grpc.health.v1.Health',
|
|
357
|
+
client,
|
|
358
|
+
callContextStorage,
|
|
359
|
+
interceptors: normalizedOptions?.interceptors,
|
|
360
|
+
staticMetadata: normalizedOptions.metadata,
|
|
361
|
+
apiKeyFnRef,
|
|
362
|
+
});
|
|
363
|
+
const healthService = HealthService.create(healthRpcImpl, false, false);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
client,
|
|
367
|
+
callContextStorage,
|
|
368
|
+
cloudService,
|
|
369
|
+
healthService,
|
|
370
|
+
options: normalizedOptions,
|
|
371
|
+
apiKeyFnRef,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Create a lazy `CloudOperationsConnection` instance.
|
|
377
|
+
*
|
|
378
|
+
* This method does not verify connectivity with the server. We recommend using {@link connect} instead.
|
|
379
|
+
*/
|
|
380
|
+
static lazy(options: CloudOperationsConnectionOptions): CloudOperationsConnection {
|
|
381
|
+
return new this(this.createCtorOptions(options));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Establish a connection with the server and return a `CloudOperationsConnection` instance.
|
|
386
|
+
*
|
|
387
|
+
* This is the preferred method of creating connections as it verifies connectivity.
|
|
388
|
+
*/
|
|
389
|
+
static async connect(options: CloudOperationsConnectionOptions): Promise<CloudOperationsConnection> {
|
|
390
|
+
const conn = this.lazy(options);
|
|
391
|
+
await conn.ensureConnected();
|
|
392
|
+
return conn;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
protected constructor({
|
|
396
|
+
options,
|
|
397
|
+
client,
|
|
398
|
+
cloudService,
|
|
399
|
+
healthService,
|
|
400
|
+
callContextStorage,
|
|
401
|
+
apiKeyFnRef,
|
|
402
|
+
}: CloudOperationsConnectionCtorOptions) {
|
|
403
|
+
this.options = options;
|
|
404
|
+
this.client = client;
|
|
405
|
+
this.cloudService = cloudService;
|
|
406
|
+
this.healthService = healthService;
|
|
407
|
+
this.callContextStorage = callContextStorage;
|
|
408
|
+
this.apiKeyFnRef = apiKeyFnRef;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
protected static generateRPCImplementation({
|
|
412
|
+
serviceName,
|
|
413
|
+
client,
|
|
414
|
+
callContextStorage,
|
|
415
|
+
interceptors,
|
|
416
|
+
staticMetadata,
|
|
417
|
+
apiKeyFnRef,
|
|
418
|
+
}: RPCImplOptions): RPCImpl {
|
|
419
|
+
return (method: { name: string }, requestData: any, callback: grpc.requestCallback<any>) => {
|
|
420
|
+
const metadataContainer = new grpc.Metadata();
|
|
421
|
+
const { metadata, deadline, abortSignal } = callContextStorage.getStore() ?? {};
|
|
422
|
+
if (apiKeyFnRef.fn) {
|
|
423
|
+
const apiKey = apiKeyFnRef.fn();
|
|
424
|
+
if (apiKey) metadataContainer.set('Authorization', `Bearer ${apiKey}`);
|
|
425
|
+
}
|
|
426
|
+
for (const [k, v] of Object.entries(staticMetadata)) {
|
|
427
|
+
metadataContainer.set(k, v);
|
|
428
|
+
}
|
|
429
|
+
if (metadata != null) {
|
|
430
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
431
|
+
metadataContainer.set(k, v);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const call = client.makeUnaryRequest(
|
|
435
|
+
`/${serviceName}/${method.name}`,
|
|
436
|
+
(arg: any) => arg,
|
|
437
|
+
(arg: any) => arg,
|
|
438
|
+
requestData,
|
|
439
|
+
metadataContainer,
|
|
440
|
+
{ interceptors, deadline },
|
|
441
|
+
callback
|
|
442
|
+
);
|
|
443
|
+
if (abortSignal != null) {
|
|
444
|
+
const listener = () => call.cancel();
|
|
445
|
+
abortSignal.addEventListener('abort', listener);
|
|
446
|
+
call.on('status', () => abortSignal.removeEventListener('abort', listener));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return call;
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Ensure connection can be established.
|
|
455
|
+
*/
|
|
456
|
+
private async ensureConnected(): Promise<void> {
|
|
457
|
+
if (this.connectPromise == null) {
|
|
458
|
+
const deadline = Date.now() + this.options.connectTimeoutMs;
|
|
459
|
+
this.connectPromise = (async () => {
|
|
460
|
+
await this.untilReady(deadline);
|
|
461
|
+
})();
|
|
462
|
+
}
|
|
463
|
+
return this.connectPromise;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Set a deadline for any service requests executed in `fn`'s scope.
|
|
468
|
+
*
|
|
469
|
+
* The deadline is a point in time after which any pending gRPC request will be considered as failed;
|
|
470
|
+
* this will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
|
|
471
|
+
* with code {@link grpc.status.DEADLINE_EXCEEDED|DEADLINE_EXCEEDED}.
|
|
472
|
+
*
|
|
473
|
+
* It is stronly recommended to explicitly set deadlines. If no deadline is set, then it is
|
|
474
|
+
* possible for the client to end up waiting forever for a response.
|
|
475
|
+
*
|
|
476
|
+
* @param deadline a point in time after which the request will be considered as failed; either a
|
|
477
|
+
* Date object, or a number of milliseconds since the Unix epoch (UTC).
|
|
478
|
+
* @returns the value returned from `fn`
|
|
479
|
+
*
|
|
480
|
+
* @see https://grpc.io/docs/guides/deadlines/
|
|
481
|
+
*/
|
|
482
|
+
public async withDeadline<ReturnType>(deadline: number | Date, fn: () => Promise<ReturnType>): Promise<ReturnType> {
|
|
483
|
+
const cc = this.callContextStorage.getStore();
|
|
484
|
+
return await this.callContextStorage.run({ ...cc, deadline }, fn);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Set an {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} that, when aborted,
|
|
489
|
+
* cancels any ongoing requests executed in `fn`'s scope.
|
|
490
|
+
*
|
|
491
|
+
* @returns value returned from `fn`
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
*
|
|
495
|
+
* ```ts
|
|
496
|
+
* const ctrl = new AbortController();
|
|
497
|
+
* setTimeout(() => ctrl.abort(), 10_000);
|
|
498
|
+
* // 👇 throws if incomplete by the timeout.
|
|
499
|
+
* await conn.withAbortSignal(ctrl.signal, () => client.cloudService.someOperation(...));
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
public async withAbortSignal<ReturnType>(
|
|
503
|
+
abortSignal: AbortSignal,
|
|
504
|
+
fn: () => Promise<ReturnType>
|
|
505
|
+
): Promise<ReturnType> {
|
|
506
|
+
const cc = this.callContextStorage.getStore();
|
|
507
|
+
return await this.callContextStorage.run({ ...cc, abortSignal }, fn);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Set metadata for any service requests executed in `fn`'s scope.
|
|
512
|
+
*
|
|
513
|
+
* The provided metadata is merged on top of any existing metadata in current scope.
|
|
514
|
+
*
|
|
515
|
+
* @returns value returned from `fn`
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
*
|
|
519
|
+
* ```ts
|
|
520
|
+
* const result = await conn.withMetadata({ someMetadata: 'value' }, () =>
|
|
521
|
+
* conn.withMetadata({ otherKey: 'set' }, () => client.cloudService.someOperation(...)))
|
|
522
|
+
* );
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
async withMetadata<ReturnType>(metadata: Metadata, fn: () => Promise<ReturnType>): Promise<ReturnType> {
|
|
526
|
+
const cc = this.callContextStorage.getStore();
|
|
527
|
+
return await this.callContextStorage.run(
|
|
528
|
+
{
|
|
529
|
+
...cc,
|
|
530
|
+
metadata: { ...cc?.metadata, ...metadata },
|
|
531
|
+
},
|
|
532
|
+
fn
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Set the apiKey for any service requests executed in `fn`'s scope (thus changing the `Authorization` header).
|
|
538
|
+
*
|
|
539
|
+
* @returns value returned from `fn`
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
*
|
|
543
|
+
* ```ts
|
|
544
|
+
* const workflowHandle = await conn.withApiKey('secret', () =>
|
|
545
|
+
* conn.withMetadata({ otherKey: 'set' }, () => client.start(options)))
|
|
546
|
+
* );
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
async withApiKey<ReturnType>(apiKey: string, fn: () => Promise<ReturnType>): Promise<ReturnType> {
|
|
550
|
+
const cc = this.callContextStorage.getStore();
|
|
551
|
+
return await this.callContextStorage.run(
|
|
552
|
+
{
|
|
553
|
+
...cc,
|
|
554
|
+
metadata: { ...cc?.metadata, Authorization: `Bearer ${apiKey}` },
|
|
555
|
+
},
|
|
556
|
+
fn
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Set the {@link ConnectionOptions.apiKey} for all subsequent requests.
|
|
562
|
+
* A static string or a callback function may be provided.
|
|
563
|
+
*/
|
|
564
|
+
setApiKey(apiKey: string | (() => string)): void {
|
|
565
|
+
if (typeof apiKey === 'string') {
|
|
566
|
+
if (apiKey === '') {
|
|
567
|
+
throw new TypeError('`apiKey` must not be an empty string');
|
|
568
|
+
}
|
|
569
|
+
this.apiKeyFnRef.fn = () => apiKey;
|
|
570
|
+
} else {
|
|
571
|
+
this.apiKeyFnRef.fn = apiKey;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Wait for successful connection to the server.
|
|
577
|
+
*
|
|
578
|
+
* @see https://grpc.github.io/grpc/node/grpc.Client.html#waitForReady__anchor
|
|
579
|
+
*/
|
|
580
|
+
protected async untilReady(deadline: number): Promise<void> {
|
|
581
|
+
return new Promise<void>((resolve, reject) => {
|
|
582
|
+
this.client.waitForReady(deadline, (err) => {
|
|
583
|
+
if (err) {
|
|
584
|
+
reject(err);
|
|
585
|
+
} else {
|
|
586
|
+
resolve();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Close the underlying gRPC client.
|
|
594
|
+
*
|
|
595
|
+
* Make sure to call this method to ensure proper resource cleanup.
|
|
596
|
+
*/
|
|
597
|
+
public close(): void {
|
|
598
|
+
this.client.close();
|
|
599
|
+
}
|
|
600
|
+
}
|
package/src/index.ts
ADDED
package/src/pkg.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ../package.json is outside of the TS project rootDir which causes TS to complain about this import.
|
|
2
|
+
// We do not want to change the rootDir because it messes up the output structure.
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import pkg from '../package.json';
|
|
6
|
+
|
|
7
|
+
export default pkg as { name: string; version: string };
|
package/src/types.ts
ADDED