@temporalio/client 0.23.0 → 1.0.0
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 +1 -1
- package/README.md +2 -2
- package/lib/async-completion-client.d.ts +12 -7
- package/lib/async-completion-client.js +16 -13
- package/lib/async-completion-client.js.map +1 -1
- package/lib/connection.d.ts +97 -47
- package/lib/connection.js +140 -25
- package/lib/connection.js.map +1 -1
- package/lib/index.d.ts +5 -3
- package/lib/index.js +8 -4
- package/lib/index.js.map +1 -1
- 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 +50 -2
- package/lib/types.js +4 -0
- package/lib/types.js.map +1 -1
- package/lib/workflow-client.d.ts +55 -15
- package/lib/workflow-client.js +73 -20
- package/lib/workflow-client.js.map +1 -1
- package/lib/workflow-options.d.ts +2 -2
- package/package.json +13 -12
- package/src/async-completion-client.ts +256 -0
- package/src/connection.ts +374 -0
- package/src/errors.ts +57 -0
- package/src/grpc-retry.ts +119 -0
- package/src/index.ts +37 -0
- package/src/interceptors.ts +132 -0
- package/src/pkg.ts +7 -0
- package/src/types.ts +88 -0
- package/src/workflow-client.ts +969 -0
- package/src/workflow-options.ts +76 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CommonWorkflowOptions, SignalDefinition,
|
|
1
|
+
import { CommonWorkflowOptions, SignalDefinition, WithCompiledWorkflowOptions } from '@temporalio/internal-workflow-common';
|
|
2
2
|
export * from '@temporalio/internal-workflow-common/lib/workflow-options';
|
|
3
|
-
export interface CompiledWorkflowOptions extends
|
|
3
|
+
export interface CompiledWorkflowOptions extends WithCompiledWorkflowOptions<WorkflowOptions> {
|
|
4
4
|
args: unknown[];
|
|
5
5
|
}
|
|
6
6
|
export interface WorkflowOptions extends CommonWorkflowOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@temporalio/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Temporal.io SDK Client sub-package",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -10,27 +10,28 @@
|
|
|
10
10
|
"workflow",
|
|
11
11
|
"client"
|
|
12
12
|
],
|
|
13
|
-
"author": "
|
|
13
|
+
"author": "Temporal Technologies Inc. <sdk@temporal.io>",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@grpc/grpc-js": "^1.
|
|
17
|
-
"@temporalio/common": "^0.
|
|
18
|
-
"@temporalio/internal-non-workflow-common": "^0.
|
|
19
|
-
"@temporalio/internal-workflow-common": "^0.
|
|
20
|
-
"@temporalio/proto": "^0.
|
|
16
|
+
"@grpc/grpc-js": "^1.6.7",
|
|
17
|
+
"@temporalio/common": "^1.0.0",
|
|
18
|
+
"@temporalio/internal-non-workflow-common": "^1.0.0",
|
|
19
|
+
"@temporalio/internal-workflow-common": "^1.0.0",
|
|
20
|
+
"@temporalio/proto": "^1.0.0",
|
|
21
21
|
"ms": "^2.1.3",
|
|
22
|
-
"protobufjs": "
|
|
22
|
+
"protobufjs": "6.11.2",
|
|
23
23
|
"uuid": "^8.3.2"
|
|
24
24
|
},
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/temporalio/sdk-typescript/issues"
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/client",
|
|
29
|
-
"files": [
|
|
30
|
-
"lib"
|
|
31
|
-
],
|
|
32
29
|
"publishConfig": {
|
|
33
30
|
"access": "public"
|
|
34
31
|
},
|
|
35
|
-
"
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"lib"
|
|
35
|
+
],
|
|
36
|
+
"gitHead": "c4fc4dc608bf58701c11b6ae02d1d63b4457718d"
|
|
36
37
|
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { Status } from '@grpc/grpc-js/build/src/constants';
|
|
2
|
+
import { DataConverter, ensureTemporalFailure, LoadedDataConverter } from '@temporalio/common';
|
|
3
|
+
import {
|
|
4
|
+
encodeErrorToFailure,
|
|
5
|
+
encodeToPayloads,
|
|
6
|
+
filterNullAndUndefined,
|
|
7
|
+
loadDataConverter,
|
|
8
|
+
} from '@temporalio/internal-non-workflow-common';
|
|
9
|
+
import { Replace } from '@temporalio/internal-workflow-common';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { Connection } from './connection';
|
|
12
|
+
import { isServerErrorResponse } from './errors';
|
|
13
|
+
import { ConnectionLike, WorkflowService } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by {@link AsyncCompletionClient} when trying to complete or heartbeat an Activity that does not exist in the
|
|
17
|
+
* system.
|
|
18
|
+
*/
|
|
19
|
+
export class ActivityNotFoundError extends Error {
|
|
20
|
+
public readonly name = 'ActivityNotFoundError';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by {@link AsyncCompletionClient} when trying to complete or heartbeat
|
|
25
|
+
* an Activity for any reason apart from {@link ActivityNotFoundError}.
|
|
26
|
+
*/
|
|
27
|
+
export class ActivityCompletionError extends Error {
|
|
28
|
+
public readonly name = 'ActivityCompletionError';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Thrown by {@link AsyncCompletionClient.heartbeat} when the Workflow has
|
|
33
|
+
* requested to cancel the reporting Activity.
|
|
34
|
+
*/
|
|
35
|
+
export class ActivityCancelledError extends Error {
|
|
36
|
+
public readonly name = 'ActivityCancelledError';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options used to configure {@link AsyncCompletionClient}
|
|
41
|
+
*/
|
|
42
|
+
export interface AsyncCompletionClientOptions {
|
|
43
|
+
/**
|
|
44
|
+
* {@link DataConverter} to use for serializing and deserializing payloads
|
|
45
|
+
*/
|
|
46
|
+
dataConverter?: DataConverter;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Identity to report to the server
|
|
50
|
+
*
|
|
51
|
+
* @default `${process.pid}@${os.hostname()}`
|
|
52
|
+
*/
|
|
53
|
+
identity?: string;
|
|
54
|
+
|
|
55
|
+
connection?: ConnectionLike;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Server namespace
|
|
59
|
+
*
|
|
60
|
+
* @default default
|
|
61
|
+
*/
|
|
62
|
+
namespace?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type AsyncCompletionClientOptionsWithDefaults = Replace<
|
|
66
|
+
Required<AsyncCompletionClientOptions>,
|
|
67
|
+
{
|
|
68
|
+
connection?: ConnectionLike;
|
|
69
|
+
}
|
|
70
|
+
>;
|
|
71
|
+
|
|
72
|
+
export function defaultAsyncCompletionClientOptions(): AsyncCompletionClientOptionsWithDefaults {
|
|
73
|
+
return {
|
|
74
|
+
dataConverter: {},
|
|
75
|
+
identity: `${process.pid}@${os.hostname()}`,
|
|
76
|
+
namespace: 'default',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A mostly unique Activity identifier including its scheduling workflow's ID
|
|
82
|
+
* and an optional runId.
|
|
83
|
+
*
|
|
84
|
+
* Activity IDs may be reused in a single Workflow run as long as a previous
|
|
85
|
+
* Activity with the same ID has completed already.
|
|
86
|
+
*/
|
|
87
|
+
export interface FullActivityId {
|
|
88
|
+
workflowId: string;
|
|
89
|
+
runId?: string;
|
|
90
|
+
activityId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A client for asynchronous completion and heartbeating of Activities.
|
|
95
|
+
*/
|
|
96
|
+
export class AsyncCompletionClient {
|
|
97
|
+
public readonly options: AsyncCompletionClientOptionsWithDefaults;
|
|
98
|
+
protected readonly dataConverter: LoadedDataConverter;
|
|
99
|
+
public readonly connection: ConnectionLike;
|
|
100
|
+
|
|
101
|
+
constructor(options?: AsyncCompletionClientOptions) {
|
|
102
|
+
this.connection = options?.connection ?? Connection.lazy();
|
|
103
|
+
this.dataConverter = loadDataConverter(options?.dataConverter);
|
|
104
|
+
this.options = { ...defaultAsyncCompletionClientOptions(), ...filterNullAndUndefined(options ?? {}) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get workflowService(): WorkflowService {
|
|
108
|
+
return this.connection.workflowService;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Transforms grpc errors into well defined TS errors.
|
|
113
|
+
*/
|
|
114
|
+
protected handleError(err: unknown): never {
|
|
115
|
+
if (isServerErrorResponse(err)) {
|
|
116
|
+
if (err.code === Status.NOT_FOUND) {
|
|
117
|
+
throw new ActivityNotFoundError('Not found');
|
|
118
|
+
}
|
|
119
|
+
throw new ActivityCompletionError(err.details || err.message);
|
|
120
|
+
}
|
|
121
|
+
throw new ActivityCompletionError('Unexpected failure');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Complete an Activity by task token
|
|
126
|
+
*/
|
|
127
|
+
async complete(taskToken: Uint8Array, result: unknown): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Complete an Activity by full ID
|
|
130
|
+
*/
|
|
131
|
+
async complete(fullActivityId: FullActivityId, result: unknown): Promise<void>;
|
|
132
|
+
|
|
133
|
+
async complete(taskTokenOrFullActivityId: Uint8Array | FullActivityId, result: unknown): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
if (taskTokenOrFullActivityId instanceof Uint8Array) {
|
|
136
|
+
await this.workflowService.respondActivityTaskCompleted({
|
|
137
|
+
identity: this.options.identity,
|
|
138
|
+
namespace: this.options.namespace,
|
|
139
|
+
taskToken: taskTokenOrFullActivityId,
|
|
140
|
+
result: { payloads: await encodeToPayloads(this.dataConverter, result) },
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
await this.workflowService.respondActivityTaskCompletedById({
|
|
144
|
+
identity: this.options.identity,
|
|
145
|
+
namespace: this.options.namespace,
|
|
146
|
+
...taskTokenOrFullActivityId,
|
|
147
|
+
result: { payloads: await encodeToPayloads(this.dataConverter, result) },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.handleError(err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fail an Activity by task token
|
|
157
|
+
*/
|
|
158
|
+
async fail(taskToken: Uint8Array, err: unknown): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* Fail an Activity by full ID
|
|
161
|
+
*/
|
|
162
|
+
async fail(fullActivityId: FullActivityId, err: unknown): Promise<void>;
|
|
163
|
+
|
|
164
|
+
async fail(taskTokenOrFullActivityId: Uint8Array | FullActivityId, err: unknown): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
if (taskTokenOrFullActivityId instanceof Uint8Array) {
|
|
167
|
+
await this.workflowService.respondActivityTaskFailed({
|
|
168
|
+
identity: this.options.identity,
|
|
169
|
+
namespace: this.options.namespace,
|
|
170
|
+
taskToken: taskTokenOrFullActivityId,
|
|
171
|
+
failure: await encodeErrorToFailure(this.dataConverter, ensureTemporalFailure(err)),
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
await this.workflowService.respondActivityTaskFailedById({
|
|
175
|
+
identity: this.options.identity,
|
|
176
|
+
namespace: this.options.namespace,
|
|
177
|
+
...taskTokenOrFullActivityId,
|
|
178
|
+
failure: await encodeErrorToFailure(this.dataConverter, err),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
this.handleError(err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Report Activity cancellation by task token
|
|
188
|
+
*/
|
|
189
|
+
reportCancellation(taskToken: Uint8Array, details?: unknown): Promise<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Report Activity cancellation by full ID
|
|
192
|
+
*/
|
|
193
|
+
reportCancellation(fullActivityId: FullActivityId, details?: unknown): Promise<void>;
|
|
194
|
+
|
|
195
|
+
async reportCancellation(taskTokenOrFullActivityId: Uint8Array | FullActivityId, details?: unknown): Promise<void> {
|
|
196
|
+
try {
|
|
197
|
+
if (taskTokenOrFullActivityId instanceof Uint8Array) {
|
|
198
|
+
await this.workflowService.respondActivityTaskCanceled({
|
|
199
|
+
identity: this.options.identity,
|
|
200
|
+
namespace: this.options.namespace,
|
|
201
|
+
taskToken: taskTokenOrFullActivityId,
|
|
202
|
+
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
await this.workflowService.respondActivityTaskCanceledById({
|
|
206
|
+
identity: this.options.identity,
|
|
207
|
+
namespace: this.options.namespace,
|
|
208
|
+
...taskTokenOrFullActivityId,
|
|
209
|
+
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
this.handleError(err);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Send Activity heartbeat by task token
|
|
219
|
+
*/
|
|
220
|
+
heartbeat(taskToken: Uint8Array, details?: unknown): Promise<void>;
|
|
221
|
+
/**
|
|
222
|
+
* Send Activity heartbeat by full ID
|
|
223
|
+
*/
|
|
224
|
+
heartbeat(fullActivityId: FullActivityId, details?: unknown): Promise<void>;
|
|
225
|
+
|
|
226
|
+
async heartbeat(taskTokenOrFullActivityId: Uint8Array | FullActivityId, details?: unknown): Promise<void> {
|
|
227
|
+
try {
|
|
228
|
+
if (taskTokenOrFullActivityId instanceof Uint8Array) {
|
|
229
|
+
const { cancelRequested } = await this.workflowService.recordActivityTaskHeartbeat({
|
|
230
|
+
identity: this.options.identity,
|
|
231
|
+
namespace: this.options.namespace,
|
|
232
|
+
taskToken: taskTokenOrFullActivityId,
|
|
233
|
+
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
|
|
234
|
+
});
|
|
235
|
+
if (cancelRequested) {
|
|
236
|
+
throw new ActivityCancelledError('cancelled');
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
const { cancelRequested } = await this.workflowService.recordActivityTaskHeartbeatById({
|
|
240
|
+
identity: this.options.identity,
|
|
241
|
+
namespace: this.options.namespace,
|
|
242
|
+
...taskTokenOrFullActivityId,
|
|
243
|
+
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
|
|
244
|
+
});
|
|
245
|
+
if (cancelRequested) {
|
|
246
|
+
throw new ActivityCancelledError('cancelled');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (err instanceof ActivityCancelledError) {
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
this.handleError(err);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import * as grpc from '@grpc/grpc-js';
|
|
2
|
+
import { filterNullAndUndefined, normalizeTlsConfig, TLSConfig } from '@temporalio/internal-non-workflow-common';
|
|
3
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
4
|
+
import type { RPCImpl } from 'protobufjs';
|
|
5
|
+
import { isServerErrorResponse, ServiceError } from './errors';
|
|
6
|
+
import { defaultGrpcRetryOptions, makeGrpcRetryInterceptor } from './grpc-retry';
|
|
7
|
+
import pkg from './pkg';
|
|
8
|
+
import { CallContext, Metadata, OperatorService, WorkflowService } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* gRPC and Temporal Server connection options
|
|
12
|
+
*/
|
|
13
|
+
export interface ConnectionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Server hostname and optional port.
|
|
16
|
+
* Port defaults to 7233 if address contains only host.
|
|
17
|
+
*
|
|
18
|
+
* @default localhost:7233
|
|
19
|
+
*/
|
|
20
|
+
address?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* TLS configuration.
|
|
24
|
+
* Pass a falsy value to use a non-encrypted connection or `true` or `{}` to
|
|
25
|
+
* connect with TLS without any customization.
|
|
26
|
+
*
|
|
27
|
+
* Either {@link credentials} or this may be specified for configuring TLS
|
|
28
|
+
*/
|
|
29
|
+
tls?: TLSConfig | boolean | null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Channel credentials, create using the factory methods defined {@link https://grpc.github.io/grpc/node/grpc.credentials.html | here}
|
|
33
|
+
*
|
|
34
|
+
* Either {@link tls} or this may be specified for configuring TLS
|
|
35
|
+
*/
|
|
36
|
+
credentials?: grpc.ChannelCredentials;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* GRPC Channel arguments
|
|
40
|
+
*
|
|
41
|
+
* @see option descriptions {@link https://grpc.github.io/grpc/core/group__grpc__arg__keys.html | here}
|
|
42
|
+
*/
|
|
43
|
+
channelArgs?: grpc.ChannelOptions;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Grpc interceptors which will be applied to every RPC call performed by this connection. By
|
|
47
|
+
* default, an interceptor will be included which automatically retries retryable errors. If you
|
|
48
|
+
* do not wish to perform automatic retries, set this to an empty list (or a list with your own
|
|
49
|
+
* interceptors).
|
|
50
|
+
*/
|
|
51
|
+
interceptors?: grpc.Interceptor[];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Optional mapping of gRPC metadata (HTTP headers) to send with each request to the server.
|
|
55
|
+
*
|
|
56
|
+
* In order to dynamically set metadata, use {@link Connection.withMetadata}
|
|
57
|
+
*/
|
|
58
|
+
metadata?: Metadata;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Milliseconds to wait until establishing a connection with the server.
|
|
62
|
+
*
|
|
63
|
+
* Used either when connecting eagerly with {@link Connection.connect} or
|
|
64
|
+
* calling {@link Connection.ensureConnected}.
|
|
65
|
+
*
|
|
66
|
+
* @format {@link https://www.npmjs.com/package/ms | ms} formatted string
|
|
67
|
+
* @default 10 seconds
|
|
68
|
+
*/
|
|
69
|
+
connectTimeout?: number | string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ConnectionOptionsWithDefaults = Required<Omit<ConnectionOptions, 'tls' | 'connectTimeout'>> & {
|
|
73
|
+
connectTimeoutMs: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const LOCAL_TARGET = '127.0.0.1:7233';
|
|
77
|
+
|
|
78
|
+
export function defaultConnectionOpts(): ConnectionOptionsWithDefaults {
|
|
79
|
+
return {
|
|
80
|
+
address: LOCAL_TARGET,
|
|
81
|
+
credentials: grpc.credentials.createInsecure(),
|
|
82
|
+
channelArgs: {},
|
|
83
|
+
interceptors: [makeGrpcRetryInterceptor(defaultGrpcRetryOptions())],
|
|
84
|
+
metadata: {},
|
|
85
|
+
connectTimeoutMs: 10_000,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* - Convert {@link ConnectionOptions.tls} to {@link grpc.ChannelCredentials}
|
|
91
|
+
* - Add the grpc.ssl_target_name_override GRPC {@link ConnectionOptions.channelArgs | channel arg}
|
|
92
|
+
* - Add default port to address if port not specified
|
|
93
|
+
*/
|
|
94
|
+
function normalizeGRPCConfig(options?: ConnectionOptions): ConnectionOptions {
|
|
95
|
+
const { tls: tlsFromConfig, credentials, ...rest } = options || {};
|
|
96
|
+
if (rest.address) {
|
|
97
|
+
// eslint-disable-next-line prefer-const
|
|
98
|
+
let [host, port] = rest.address.split(':', 2);
|
|
99
|
+
port = port || '7233';
|
|
100
|
+
rest.address = `${host}:${port}`;
|
|
101
|
+
}
|
|
102
|
+
const tls = normalizeTlsConfig(tlsFromConfig);
|
|
103
|
+
if (tls) {
|
|
104
|
+
if (credentials) {
|
|
105
|
+
throw new TypeError('Both `tls` and `credentials` ConnectionOptions were provided');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
...rest,
|
|
109
|
+
credentials: grpc.credentials.createSsl(
|
|
110
|
+
tls.serverRootCACertificate,
|
|
111
|
+
tls.clientCertPair?.key,
|
|
112
|
+
tls.clientCertPair?.crt
|
|
113
|
+
),
|
|
114
|
+
channelArgs: {
|
|
115
|
+
...rest.channelArgs,
|
|
116
|
+
...(tls.serverNameOverride
|
|
117
|
+
? {
|
|
118
|
+
'grpc.ssl_target_name_override': tls.serverNameOverride,
|
|
119
|
+
'grpc.default_authority': tls.serverNameOverride,
|
|
120
|
+
}
|
|
121
|
+
: undefined),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
} else {
|
|
125
|
+
return rest;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface RPCImplOptions {
|
|
130
|
+
serviceName: string;
|
|
131
|
+
client: grpc.Client;
|
|
132
|
+
callContextStorage: AsyncLocalStorage<CallContext>;
|
|
133
|
+
interceptors?: grpc.Interceptor[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ConnectionCtorOptions {
|
|
137
|
+
readonly options: ConnectionOptionsWithDefaults;
|
|
138
|
+
readonly client: grpc.Client;
|
|
139
|
+
/**
|
|
140
|
+
* Raw gRPC access to the Temporal service.
|
|
141
|
+
*
|
|
142
|
+
* **NOTE**: The namespace provided in {@link options} is **not** automatically set on requests made to the service.
|
|
143
|
+
*/
|
|
144
|
+
readonly workflowService: WorkflowService;
|
|
145
|
+
readonly operatorService: OperatorService;
|
|
146
|
+
readonly callContextStorage: AsyncLocalStorage<CallContext>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Client connection to the Temporal Server
|
|
151
|
+
*
|
|
152
|
+
* ⚠️ Connections are expensive to construct and should be reused. Make sure to {@link close} any unused connections to
|
|
153
|
+
* avoid leaking resources.
|
|
154
|
+
*/
|
|
155
|
+
export class Connection {
|
|
156
|
+
/**
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
public static readonly Client = grpc.makeGenericClientConstructor({}, 'WorkflowService', {});
|
|
160
|
+
|
|
161
|
+
public readonly options: ConnectionOptionsWithDefaults;
|
|
162
|
+
protected readonly client: grpc.Client;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Used to ensure `ensureConnected` is called once.
|
|
166
|
+
*/
|
|
167
|
+
protected connectPromise?: Promise<void>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Raw gRPC access to Temporal Server's {@link
|
|
171
|
+
* https://github.com/temporalio/api/blob/master/temporal/api/workflowservice/v1/service.proto | Workflow service}
|
|
172
|
+
*/
|
|
173
|
+
public readonly workflowService: WorkflowService;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Raw gRPC access to Temporal Server's
|
|
177
|
+
* {@link https://github.com/temporalio/api/blob/master/temporal/api/operatorservice/v1/service.proto | Operator service}
|
|
178
|
+
*/
|
|
179
|
+
public readonly operatorService: OperatorService;
|
|
180
|
+
readonly callContextStorage: AsyncLocalStorage<CallContext>;
|
|
181
|
+
|
|
182
|
+
protected static createCtorOptions(options?: ConnectionOptions): ConnectionCtorOptions {
|
|
183
|
+
const optionsWithDefaults = {
|
|
184
|
+
...defaultConnectionOpts(),
|
|
185
|
+
...filterNullAndUndefined(normalizeGRPCConfig(options)),
|
|
186
|
+
};
|
|
187
|
+
// Allow overriding this
|
|
188
|
+
optionsWithDefaults.metadata['client-name'] ??= 'temporal-typescript';
|
|
189
|
+
optionsWithDefaults.metadata['client-version'] ??= pkg.version;
|
|
190
|
+
|
|
191
|
+
const client = new this.Client(
|
|
192
|
+
optionsWithDefaults.address,
|
|
193
|
+
optionsWithDefaults.credentials,
|
|
194
|
+
optionsWithDefaults.channelArgs
|
|
195
|
+
);
|
|
196
|
+
const callContextStorage = new AsyncLocalStorage<CallContext>();
|
|
197
|
+
callContextStorage.enterWith({ metadata: optionsWithDefaults.metadata });
|
|
198
|
+
|
|
199
|
+
const workflowRpcImpl = this.generateRPCImplementation({
|
|
200
|
+
serviceName: 'temporal.api.workflowservice.v1.WorkflowService',
|
|
201
|
+
client,
|
|
202
|
+
callContextStorage,
|
|
203
|
+
interceptors: optionsWithDefaults?.interceptors,
|
|
204
|
+
});
|
|
205
|
+
const workflowService = WorkflowService.create(workflowRpcImpl, false, false);
|
|
206
|
+
const operatorRpcImpl = this.generateRPCImplementation({
|
|
207
|
+
serviceName: 'temporal.api.operatorservice.v1.OperatorService',
|
|
208
|
+
client,
|
|
209
|
+
callContextStorage,
|
|
210
|
+
interceptors: optionsWithDefaults?.interceptors,
|
|
211
|
+
});
|
|
212
|
+
const operatorService = OperatorService.create(operatorRpcImpl, false, false);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
client,
|
|
216
|
+
callContextStorage,
|
|
217
|
+
workflowService,
|
|
218
|
+
operatorService,
|
|
219
|
+
options: optionsWithDefaults,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Ensure connection can be established.
|
|
225
|
+
*
|
|
226
|
+
* Does not need to be called if you use {@link connect}.
|
|
227
|
+
*
|
|
228
|
+
* This method's result is memoized to ensure it runs only once.
|
|
229
|
+
*
|
|
230
|
+
* Calls {@link proto.temporal.api.workflowservice.v1.WorkflowService.getSystemInfo} internally.
|
|
231
|
+
*/
|
|
232
|
+
async ensureConnected(): Promise<void> {
|
|
233
|
+
if (this.connectPromise == null) {
|
|
234
|
+
const deadline = Date.now() + this.options.connectTimeoutMs;
|
|
235
|
+
this.connectPromise = (async () => {
|
|
236
|
+
await this.untilReady(deadline);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
await this.withDeadline(deadline, () => this.workflowService.getSystemInfo({}));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (isServerErrorResponse(err)) {
|
|
242
|
+
// Ignore old servers
|
|
243
|
+
if (err.code !== grpc.status.UNIMPLEMENTED) {
|
|
244
|
+
throw new ServiceError('Failed to connect to Temporal server', { cause: err });
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
}
|
|
252
|
+
return this.connectPromise;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Create a lazy `Connection` instance.
|
|
257
|
+
*
|
|
258
|
+
* This method does not verify connectivity with the server. We recommend using {@link connect} instead.
|
|
259
|
+
*/
|
|
260
|
+
static lazy(options?: ConnectionOptions): Connection {
|
|
261
|
+
return new this(this.createCtorOptions(options));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Establish a connection with the server and return a `Connection` instance.
|
|
266
|
+
*
|
|
267
|
+
* This is the preferred method of creating connections as it verifies connectivity by calling
|
|
268
|
+
* {@link ensureConnected}.
|
|
269
|
+
*/
|
|
270
|
+
static async connect(options?: ConnectionOptions): Promise<Connection> {
|
|
271
|
+
const conn = this.lazy(options);
|
|
272
|
+
await conn.ensureConnected();
|
|
273
|
+
return conn;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
protected constructor({
|
|
277
|
+
options,
|
|
278
|
+
client,
|
|
279
|
+
workflowService,
|
|
280
|
+
operatorService,
|
|
281
|
+
callContextStorage,
|
|
282
|
+
}: ConnectionCtorOptions) {
|
|
283
|
+
this.options = options;
|
|
284
|
+
this.client = client;
|
|
285
|
+
this.workflowService = workflowService;
|
|
286
|
+
this.operatorService = operatorService;
|
|
287
|
+
this.callContextStorage = callContextStorage;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected static generateRPCImplementation({
|
|
291
|
+
serviceName,
|
|
292
|
+
client,
|
|
293
|
+
callContextStorage,
|
|
294
|
+
interceptors,
|
|
295
|
+
}: RPCImplOptions): RPCImpl {
|
|
296
|
+
return (method: { name: string }, requestData: any, callback: grpc.requestCallback<any>) => {
|
|
297
|
+
const metadataContainer = new grpc.Metadata();
|
|
298
|
+
const { metadata, deadline } = callContextStorage.getStore() ?? {};
|
|
299
|
+
if (metadata != null) {
|
|
300
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
301
|
+
metadataContainer.set(k, v);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return client.makeUnaryRequest(
|
|
305
|
+
`/${serviceName}/${method.name}`,
|
|
306
|
+
(arg: any) => arg,
|
|
307
|
+
(arg: any) => arg,
|
|
308
|
+
requestData,
|
|
309
|
+
metadataContainer,
|
|
310
|
+
{ interceptors, deadline },
|
|
311
|
+
callback
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Set the deadline for any service requests executed in `fn`'s scope.
|
|
318
|
+
*
|
|
319
|
+
* @returns value returned from `fn`
|
|
320
|
+
*/
|
|
321
|
+
async withDeadline<ReturnType>(deadline: number | Date, fn: () => Promise<ReturnType>): Promise<ReturnType> {
|
|
322
|
+
const cc = this.callContextStorage.getStore();
|
|
323
|
+
return await this.callContextStorage.run({ deadline, metadata: cc?.metadata }, fn);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Set metadata for any service requests executed in `fn`'s scope.
|
|
328
|
+
*
|
|
329
|
+
* The provided metadata is merged on top of any existing metadata in current scope, including metadata provided in
|
|
330
|
+
* {@link ConnectionOptions.metadata}.
|
|
331
|
+
*
|
|
332
|
+
* @returns value returned from `fn`
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
*
|
|
336
|
+
*```ts
|
|
337
|
+
*const workflowHandle = await conn.withMetadata({ apiKey: 'secret' }, () =>
|
|
338
|
+
* conn.withMetadata({ otherKey: 'set' }, () => client.start(options)))
|
|
339
|
+
*);
|
|
340
|
+
*```
|
|
341
|
+
*/
|
|
342
|
+
async withMetadata<ReturnType>(metadata: Metadata, fn: () => Promise<ReturnType>): Promise<ReturnType> {
|
|
343
|
+
const cc = this.callContextStorage.getStore();
|
|
344
|
+
metadata = { ...cc?.metadata, ...metadata };
|
|
345
|
+
return await this.callContextStorage.run({ metadata, deadline: cc?.deadline }, fn);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Wait for successful connection to the server.
|
|
350
|
+
*
|
|
351
|
+
* @see https://grpc.github.io/grpc/node/grpc.Client.html#waitForReady__anchor
|
|
352
|
+
*/
|
|
353
|
+
protected async untilReady(deadline: number): Promise<void> {
|
|
354
|
+
return new Promise<void>((resolve, reject) => {
|
|
355
|
+
this.client.waitForReady(deadline, (err) => {
|
|
356
|
+
if (err) {
|
|
357
|
+
reject(err);
|
|
358
|
+
} else {
|
|
359
|
+
resolve();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// This method is async for uniformity with NativeConnection which could be used in the future to power clients
|
|
366
|
+
/**
|
|
367
|
+
* Close the underlying gRPC client.
|
|
368
|
+
*
|
|
369
|
+
* Make sure to call this method to ensure proper resource cleanup.
|
|
370
|
+
*/
|
|
371
|
+
public async close(): Promise<void> {
|
|
372
|
+
this.client.close();
|
|
373
|
+
}
|
|
374
|
+
}
|