Skip to content

Commit 600a06f

Browse files
authored
feat(client): Add experimental support for Cloud Operations API (#1538)
1 parent bfcf392 commit 600a06f

23 files changed

+879
-43
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,12 @@ jobs:
286286
RUN_INTEGRATION_TESTS: true
287287
REUSE_V8_CONTEXT: ${{ matrix.reuse-v8-context }}
288288

289+
# Cloud Tests will be skipped if TEMPORAL_CLIENT_CLOUD_API_KEY is left empty
290+
TEMPORAL_CLOUD_SAAS_ADDRESS: ${{ vars.TEMPORAL_CLOUD_SAAS_ADDRESS || 'saas-api.tmprl.cloud:443' }}
291+
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
292+
TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00
293+
TEMPORAL_CLIENT_CLOUD_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}
294+
289295
- name: Upload NPM logs
290296
uses: actions/upload-artifact@v4
291297
if: failure() || cancelled()

package-lock.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"scripts": {
2424
"rebuild": "npm run clean && npm run build",
2525
"build": "lerna run --stream build",
26-
"build.watch": "lerna run --stream build.watch",
26+
"build.watch": "npm run build:protos && tsc --build --watch packages/*",
27+
"build:protos": "node ./packages/proto/scripts/compile-proto.js",
2728
"test": "lerna run --stream test",
2829
"test.watch": "lerna run --stream test.watch",
2930
"ci-stress": "node ./packages/test/lib/load/run-all-stress-ci-scenarios.js",
@@ -38,6 +39,7 @@
3839
},
3940
"dependencies": {
4041
"@temporalio/client": "file:packages/client",
42+
"@temporalio/cloud": "file:packages/cloud",
4143
"@temporalio/common": "file:packages/common",
4244
"@temporalio/create": "file:packages/create-project",
4345
"@temporalio/interceptors-opentelemetry": "file:packages/interceptors-opentelemetry",
@@ -79,6 +81,7 @@
7981
"workspaces": [
8082
"packages/activity",
8183
"packages/client",
84+
"packages/cloud",
8285
"packages/common",
8386
"packages/core-bridge",
8487
"packages/create-project",

packages/client/src/base-client.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os from 'node:os';
2+
import type * as _grpc from '@grpc/grpc-js'; // For JSDoc only
23
import { DataConverter, LoadedDataConverter } from '@temporalio/common';
34
import { isLoadedDataConverter, loadDataConverter } from '@temporalio/common/lib/internal-non-workflow';
45
import { Connection } from './connection';
@@ -51,7 +52,14 @@ export function defaultBaseClientOptions(): WithDefaults<BaseClientOptions> {
5152
}
5253

5354
export class BaseClient {
55+
/**
56+
* The underlying {@link Connection | connection} used by this client.
57+
*
58+
* Clients are cheap to create, but connections are expensive. Where that make sense,
59+
* a single connection may and should be reused by multiple `Client`.
60+
*/
5461
public readonly connection: ConnectionLike;
62+
5563
private readonly loadedDataConverter: LoadedDataConverter;
5664

5765
protected constructor(options?: BaseClientOptions) {
@@ -61,19 +69,37 @@ export class BaseClient {
6169
}
6270

6371
/**
64-
* Set the deadline for any service requests executed in `fn`'s scope.
72+
* Set a deadline for any service requests executed in `fn`'s scope.
73+
*
74+
* The deadline is a point in time after which any pending gRPC request will be considered as failed;
75+
* this will locally result in the request call throwing a {@link _grpc.ServiceError|ServiceError}
76+
* with code {@link _grpc.status.DEADLINE_EXCEEDED|DEADLINE_EXCEEDED}.
77+
*
78+
* It is stronly recommended to explicitly set deadlines. If no deadline is set, then it is
79+
* possible for the client to end up waiting forever for a response.
80+
*
81+
* This method is only a convenience wrapper around {@link Connection.withDeadline}.
82+
*
83+
* @param deadline a point in time after which the request will be considered as failed; either a
84+
* Date object, or a number of milliseconds since the Unix epoch (UTC).
85+
* @returns the value returned from `fn`
86+
*
87+
* @see https://grpc.io/docs/guides/deadlines/
6588
*/
6689
public async withDeadline<R>(deadline: number | Date, fn: () => Promise<R>): Promise<R> {
6790
return await this.connection.withDeadline(deadline, fn);
6891
}
6992

7093
/**
71-
* Set an {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} that, when aborted,
72-
* cancels any ongoing service requests executed in `fn`'s scope.
94+
* Set an {@link AbortSignal} that, when aborted, cancels any ongoing service requests executed in
95+
* `fn`'s scope. This will locally result in the request call throwing a {@link _grpc.ServiceError|ServiceError}
96+
* with code {@link _grpc.status.CANCELLED|CANCELLED}.
97+
*
98+
* This method is only a convenience wrapper around {@link Connection.withAbortSignal}.
7399
*
74100
* @returns value returned from `fn`
75101
*
76-
* @see {@link Connection.withAbortSignal}
102+
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
77103
*/
78104
async withAbortSignal<R>(abortSignal: AbortSignal, fn: () => Promise<R>): Promise<R> {
79105
return await this.connection.withAbortSignal(abortSignal, fn);
@@ -82,9 +108,9 @@ export class BaseClient {
82108
/**
83109
* Set metadata for any service requests executed in `fn`'s scope.
84110
*
85-
* @returns returned value of `fn`
111+
* This method is only a convenience wrapper around {@link Connection.withMetadata}.
86112
*
87-
* @see {@link Connection.withMetadata}
113+
* @returns returned value of `fn`
88114
*/
89115
public async withMetadata<R>(metadata: Metadata, fn: () => Promise<R>): Promise<R> {
90116
return await this.connection.withMetadata(metadata, fn);

packages/client/src/connection.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ import {
55
filterNullAndUndefined,
66
normalizeTlsConfig,
77
TLSConfig,
8-
normalizeTemporalGrpcEndpointAddress,
8+
normalizeGrpcEndpointAddress,
99
} from '@temporalio/common/lib/internal-non-workflow';
1010
import { Duration, msOptionalToNumber } from '@temporalio/common/lib/time';
1111
import { isGrpcServiceError, ServiceError } from './errors';
1212
import { defaultGrpcRetryOptions, makeGrpcRetryInterceptor } from './grpc-retry';
1313
import pkg from './pkg';
1414
import { CallContext, HealthService, Metadata, OperatorService, WorkflowService } from './types';
1515

16+
/**
17+
* The default Temporal Server's TCP port for public gRPC connections.
18+
*/
19+
const DEFAULT_TEMPORAL_GRPC_PORT = 7233;
20+
1621
/**
1722
* gRPC and Temporal Server connection options
1823
*/
@@ -174,7 +179,7 @@ function normalizeGRPCConfig(options?: ConnectionOptions): ConnectionOptions {
174179
}
175180
}
176181
if (rest.address) {
177-
rest.address = normalizeTemporalGrpcEndpointAddress(rest.address);
182+
rest.address = normalizeGrpcEndpointAddress(rest.address, DEFAULT_TEMPORAL_GRPC_PORT);
178183
}
179184
const tls = normalizeTlsConfig(tlsFromConfig);
180185
if (tls) {
@@ -220,29 +225,33 @@ export interface RPCImplOptions {
220225
export interface ConnectionCtorOptions {
221226
readonly options: ConnectionOptionsWithDefaults;
222227
readonly client: grpc.Client;
228+
223229
/**
224230
* Raw gRPC access to the Temporal service.
225231
*
226232
* **NOTE**: The namespace provided in {@link options} is **not** automatically set on requests made to the service.
227233
*/
228234
readonly workflowService: WorkflowService;
235+
229236
/**
230237
* Raw gRPC access to the Temporal {@link https://github.com/temporalio/api/blob/ddf07ab9933e8230309850e3c579e1ff34b03f53/temporal/api/operatorservice/v1/service.proto | operator service}.
231238
*/
232239
readonly operatorService: OperatorService;
240+
233241
/**
234242
* Raw gRPC access to the standard gRPC {@link https://github.com/grpc/grpc/blob/92f58c18a8da2728f571138c37760a721c8915a2/doc/health-checking.md | health service}.
235243
*/
236244
readonly healthService: HealthService;
245+
237246
readonly callContextStorage: AsyncLocalStorage<CallContext>;
238247
readonly apiKeyFnRef: { fn?: () => string };
239248
}
240249

241250
/**
242251
* Client connection to the Temporal Server
243252
*
244-
* ⚠️ Connections are expensive to construct and should be reused. Make sure to {@link close} any unused connections to
245-
* avoid leaking resources.
253+
* ⚠️ Connections are expensive to construct and should be reused.
254+
* Make sure to {@link close} any unused connections to avoid leaking resources.
246255
*/
247256
export class Connection {
248257
/**
@@ -275,7 +284,12 @@ export class Connection {
275284
* Cloud namespace will result in gRPC `unauthorized` error.
276285
*/
277286
public readonly operatorService: OperatorService;
287+
288+
/**
289+
* Raw gRPC access to the standard gRPC {@link https://github.com/grpc/grpc/blob/92f58c18a8da2728f571138c37760a721c8915a2/doc/health-checking.md | health service}.
290+
*/
278291
public readonly healthService: HealthService;
292+
279293
readonly callContextStorage: AsyncLocalStorage<CallContext>;
280294
private readonly apiKeyFnRef: { fn?: () => string };
281295

@@ -424,7 +438,8 @@ export class Connection {
424438
const metadataContainer = new grpc.Metadata();
425439
const { metadata, deadline, abortSignal } = callContextStorage.getStore() ?? {};
426440
if (apiKeyFnRef.fn) {
427-
metadataContainer.set('Authorization', `Bearer ${apiKeyFnRef.fn()}`);
441+
const apiKey = apiKeyFnRef.fn();
442+
if (apiKey) metadataContainer.set('Authorization', `Bearer ${apiKey}`);
428443
}
429444
for (const [k, v] of Object.entries(staticMetadata)) {
430445
metadataContainer.set(k, v);
@@ -452,20 +467,32 @@ export class Connection {
452467
}
453468

454469
/**
455-
* Set the deadline for any service requests executed in `fn`'s scope.
470+
* Set a deadline for any service requests executed in `fn`'s scope.
456471
*
457-
* @returns value returned from `fn`
472+
* The deadline is a point in time after which any pending gRPC request will be considered as failed;
473+
* this will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
474+
* with code {@link grpc.status.DEADLINE_EXCEEDED|DEADLINE_EXCEEDED}.
475+
*
476+
* It is stronly recommended to explicitly set deadlines. If no deadline is set, then it is
477+
* possible for the client to end up waiting forever for a response.
478+
*
479+
* @param deadline a point in time after which the request will be considered as failed; either a
480+
* Date object, or a number of milliseconds since the Unix epoch (UTC).
481+
* @returns the value returned from `fn`
482+
*
483+
* @see https://grpc.io/docs/guides/deadlines/
458484
*/
459485
async withDeadline<ReturnType>(deadline: number | Date, fn: () => Promise<ReturnType>): Promise<ReturnType> {
460486
const cc = this.callContextStorage.getStore();
461487
return await this.callContextStorage.run({ ...cc, deadline }, fn);
462488
}
463489

464490
/**
465-
* Set an {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} that, when aborted,
466-
* cancels any ongoing requests executed in `fn`'s scope.
491+
* Set an {@link AbortSignal} that, when aborted, cancels any ongoing service requests executed in
492+
* `fn`'s scope. This will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
493+
* with code {@link grpc.status.CANCELLED|CANCELLED}.
467494
*
468-
* @returns value returned from `fn`
495+
* This method is only a convenience wrapper around {@link Connection.withAbortSignal}.
469496
*
470497
* @example
471498
*
@@ -475,6 +502,10 @@ export class Connection {
475502
* // 👇 throws if incomplete by the timeout.
476503
* await conn.withAbortSignal(ctrl.signal, () => client.workflow.execute(myWorkflow, options));
477504
* ```
505+
*
506+
* @returns value returned from `fn`
507+
*
508+
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
478509
*/
479510
async withAbortSignal<ReturnType>(abortSignal: AbortSignal, fn: () => Promise<ReturnType>): Promise<ReturnType> {
480511
const cc = this.callContextStorage.getStore();
@@ -572,5 +603,6 @@ export class Connection {
572603
*/
573604
public async close(): Promise<void> {
574605
this.client.close();
606+
this.callContextStorage.disable();
575607
}
576608
}

packages/client/src/types.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,24 @@ export interface ConnectionLike {
9696
workflowService: WorkflowService;
9797
close(): Promise<void>;
9898
ensureConnected(): Promise<void>;
99+
99100
/**
100-
* Set the deadline for any service requests executed in `fn`'s scope.
101+
* Set a deadline for any service requests executed in `fn`'s scope.
102+
*
103+
* The deadline is a point in time after which any pending gRPC request will be considered as failed;
104+
* this will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
105+
* with code {@link grpc.status.DEADLINE_EXCEEDED|DEADLINE_EXCEEDED}.
106+
*
107+
* It is stronly recommended to explicitly set deadlines. If no deadline is set, then it is
108+
* possible for the client to end up waiting forever for a response.
109+
*
110+
* This method is only a convenience wrapper around {@link Connection.withDeadline}.
111+
*
112+
* @param deadline a point in time after which the request will be considered as failed; either a
113+
* Date object, or a number of milliseconds since the Unix epoch (UTC).
114+
* @returns the value returned from `fn`
115+
*
116+
* @see https://grpc.io/docs/guides/deadlines/
101117
*/
102118
withDeadline<R>(deadline: number | Date, fn: () => Promise<R>): Promise<R>;
103119

@@ -109,10 +125,13 @@ export interface ConnectionLike {
109125
withMetadata<R>(metadata: Metadata, fn: () => Promise<R>): Promise<R>;
110126

111127
/**
112-
* Set an {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} that, when aborted,
113-
* cancels any ongoing requests executed in `fn`'s scope.
128+
* Set an {@link AbortSignal} that, when aborted, cancels any ongoing service requests executed in
129+
* `fn`'s scope. This will locally result in the request call throwing a {@link grpc.ServiceError|ServiceError}
130+
* with code {@link grpc.status.CANCELLED|CANCELLED}.
114131
*
115132
* @returns value returned from `fn`
133+
*
134+
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
116135
*/
117136
withAbortSignal<R>(abortSignal: AbortSignal, fn: () => Promise<R>): Promise<R>;
118137
}

packages/cloud/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# `@temporalio/cloud`
2+
3+
[![NPM](https://img.shields.io/npm/v/@temporalio/cloud?style=for-the-badge)](https://www.npmjs.com/package/@temporalio/cloud)
4+
5+
Part of [Temporal](https://temporal.io)'s [TypeScript SDK](https://docs.temporal.io/typescript/introduction/).
6+
7+
- [API reference](https://typescript.temporal.io/api/namespaces/cloud)
8+
- [Sample projects](https://github.com/temporalio/samples-typescript)

0 commit comments

Comments
 (0)