@temporalio/client 1.0.0-rc.0 → 1.0.0-rc.1
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/lib/async-completion-client.d.ts +3 -3
- package/lib/async-completion-client.js +3 -3
- package/lib/connection.d.ts +33 -21
- package/lib/connection.js +24 -17
- package/lib/connection.js.map +1 -1
- package/lib/index.d.ts +4 -2
- package/lib/index.js +4 -2
- package/lib/index.js.map +1 -1
- package/lib/workflow-client.d.ts +3 -3
- package/lib/workflow-client.js +3 -3
- package/package.json +12 -11
- 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
|
@@ -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
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ServerErrorResponse } from '@grpc/grpc-js';
|
|
2
|
+
import { RetryState, TemporalFailure } from '@temporalio/common';
|
|
3
|
+
export { WorkflowExecutionAlreadyStartedError } from '@temporalio/common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic Error class for errors coming from the service
|
|
7
|
+
*/
|
|
8
|
+
export class ServiceError extends Error {
|
|
9
|
+
public readonly name: string = 'ServiceError';
|
|
10
|
+
public readonly cause?: Error;
|
|
11
|
+
|
|
12
|
+
constructor(message: string, opts?: { cause: Error }) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.cause = opts?.cause;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Thrown by the client while waiting on Workflow execution result if execution
|
|
20
|
+
* completes with failure.
|
|
21
|
+
*
|
|
22
|
+
* The failure type will be set in the `cause` attribute.
|
|
23
|
+
*
|
|
24
|
+
* For example if the workflow is cancelled, `cause` will be set to
|
|
25
|
+
* {@link CancelledFailure}.
|
|
26
|
+
*/
|
|
27
|
+
export class WorkflowFailedError extends Error {
|
|
28
|
+
public readonly name: string = 'WorkflowFailedError';
|
|
29
|
+
public constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
public readonly cause: TemporalFailure | undefined,
|
|
32
|
+
public readonly retryState: RetryState
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Thrown the by client while waiting on Workflow execution result if Workflow
|
|
40
|
+
* continues as new.
|
|
41
|
+
*
|
|
42
|
+
* Only thrown if asked not to follow the chain of execution (see {@link WorkflowOptions.followRuns}).
|
|
43
|
+
*/
|
|
44
|
+
export class WorkflowContinuedAsNewError extends Error {
|
|
45
|
+
public readonly name: string = 'WorkflowExecutionContinuedAsNewError';
|
|
46
|
+
public constructor(message: string, public readonly newExecutionRunId: string) {
|
|
47
|
+
super(message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Type assertion helper, assertion is mostly empty because any additional
|
|
53
|
+
* properties are optional.
|
|
54
|
+
*/
|
|
55
|
+
export function isServerErrorResponse(err: unknown): err is ServerErrorResponse {
|
|
56
|
+
return err instanceof Error;
|
|
57
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InterceptingCall,
|
|
3
|
+
Interceptor,
|
|
4
|
+
ListenerBuilder,
|
|
5
|
+
Metadata,
|
|
6
|
+
RequesterBuilder,
|
|
7
|
+
StatusObject,
|
|
8
|
+
} from '@grpc/grpc-js';
|
|
9
|
+
import * as grpc from '@grpc/grpc-js';
|
|
10
|
+
|
|
11
|
+
export interface GrpcRetryOptions {
|
|
12
|
+
/** Maximum number of allowed retries. Defaults to 10. */
|
|
13
|
+
maxRetries: number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A function which accepts the current retry attempt (starts at 0) and returns the millisecond
|
|
17
|
+
* delay that should be applied before the next retry.
|
|
18
|
+
*/
|
|
19
|
+
delayFunction: (attempt: number) => number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A function which accepts a failed status object and returns true if the call should be retried
|
|
23
|
+
*/
|
|
24
|
+
retryableDecider: (status: StatusObject) => boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function defaultGrpcRetryOptions(): GrpcRetryOptions {
|
|
28
|
+
return {
|
|
29
|
+
maxRetries: 10,
|
|
30
|
+
delayFunction: backOffAmount,
|
|
31
|
+
retryableDecider: isRetryableError,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const retryableCodes = new Set([
|
|
36
|
+
grpc.status.UNKNOWN,
|
|
37
|
+
grpc.status.RESOURCE_EXHAUSTED,
|
|
38
|
+
grpc.status.UNAVAILABLE,
|
|
39
|
+
grpc.status.ABORTED,
|
|
40
|
+
grpc.status.DATA_LOSS,
|
|
41
|
+
grpc.status.OUT_OF_RANGE,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export function isRetryableError(status: StatusObject): boolean {
|
|
45
|
+
return retryableCodes.has(status.code);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Return backoff amount in ms */
|
|
49
|
+
export function backOffAmount(attempt: number): number {
|
|
50
|
+
return 2 ** attempt * 20;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns a GRPC interceptor that will perform automatic retries for some types of failed calls
|
|
55
|
+
*
|
|
56
|
+
* @param retryOptions Options for the retry interceptor
|
|
57
|
+
*/
|
|
58
|
+
export function makeGrpcRetryInterceptor(retryOptions: GrpcRetryOptions): Interceptor {
|
|
59
|
+
return (options, nextCall) => {
|
|
60
|
+
let savedMetadata: Metadata;
|
|
61
|
+
let savedSendMessage: any;
|
|
62
|
+
let savedReceiveMessage: any;
|
|
63
|
+
let savedMessageNext: any;
|
|
64
|
+
const requester = new RequesterBuilder()
|
|
65
|
+
.withStart(function (metadata, _listener, next) {
|
|
66
|
+
savedMetadata = metadata;
|
|
67
|
+
const newListener = new ListenerBuilder()
|
|
68
|
+
.withOnReceiveMessage((message, next) => {
|
|
69
|
+
savedReceiveMessage = message;
|
|
70
|
+
savedMessageNext = next;
|
|
71
|
+
})
|
|
72
|
+
.withOnReceiveStatus((status, next) => {
|
|
73
|
+
let retries = 0;
|
|
74
|
+
const retry = (message: any, metadata: Metadata) => {
|
|
75
|
+
retries++;
|
|
76
|
+
const newCall = nextCall(options);
|
|
77
|
+
newCall.start(metadata, {
|
|
78
|
+
onReceiveMessage: (message) => {
|
|
79
|
+
savedReceiveMessage = message;
|
|
80
|
+
},
|
|
81
|
+
onReceiveStatus: (status) => {
|
|
82
|
+
if (retryOptions.retryableDecider(status)) {
|
|
83
|
+
if (retries <= retryOptions.maxRetries) {
|
|
84
|
+
setTimeout(() => retry(message, metadata), retryOptions.delayFunction(retries));
|
|
85
|
+
} else {
|
|
86
|
+
savedMessageNext(savedReceiveMessage);
|
|
87
|
+
next(status);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
savedMessageNext(savedReceiveMessage);
|
|
91
|
+
// TODO: For reasons that are completely unclear to me, if you pass a handcrafted
|
|
92
|
+
// status object here, node will magically just exit at the end of this line.
|
|
93
|
+
// No warning, no nothing. Here be dragons.
|
|
94
|
+
next(status);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
newCall.sendMessage(message);
|
|
99
|
+
newCall.halfClose();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (retryOptions.retryableDecider(status)) {
|
|
103
|
+
setTimeout(() => retry(savedSendMessage, savedMetadata), backOffAmount(retries));
|
|
104
|
+
} else {
|
|
105
|
+
savedMessageNext(savedReceiveMessage);
|
|
106
|
+
next(status);
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.build();
|
|
110
|
+
next(metadata, newListener);
|
|
111
|
+
})
|
|
112
|
+
.withSendMessage((message, next) => {
|
|
113
|
+
savedSendMessage = message;
|
|
114
|
+
next(message);
|
|
115
|
+
})
|
|
116
|
+
.build();
|
|
117
|
+
return new InterceptingCall(nextCall(options), requester);
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for communicating with Temporal Server.
|
|
3
|
+
*
|
|
4
|
+
* Most functionality is available through {@link WorkflowClient}, but you can also call gRPC methods directly using {@link Connection.workflowService} and {@link Connection.operatorService}.
|
|
5
|
+
*
|
|
6
|
+
* ### Usage
|
|
7
|
+
* <!--SNIPSTART typescript-hello-client-->
|
|
8
|
+
* <!--SNIPEND-->
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
ActivityFailure,
|
|
14
|
+
ApplicationFailure,
|
|
15
|
+
CancelledFailure,
|
|
16
|
+
ChildWorkflowFailure,
|
|
17
|
+
DataConverter,
|
|
18
|
+
defaultPayloadConverter,
|
|
19
|
+
ProtoFailure,
|
|
20
|
+
ServerFailure,
|
|
21
|
+
TemporalFailure,
|
|
22
|
+
TerminatedFailure,
|
|
23
|
+
TimeoutFailure,
|
|
24
|
+
} from '@temporalio/common';
|
|
25
|
+
export { TLSConfig } from '@temporalio/internal-non-workflow-common';
|
|
26
|
+
export { RetryPolicy } from '@temporalio/internal-workflow-common';
|
|
27
|
+
export * from '@temporalio/internal-workflow-common/lib/errors';
|
|
28
|
+
export * from '@temporalio/internal-workflow-common/lib/interfaces';
|
|
29
|
+
export * from '@temporalio/internal-workflow-common/lib/workflow-handle';
|
|
30
|
+
export * from './async-completion-client';
|
|
31
|
+
export { Connection, ConnectionOptions, ConnectionOptionsWithDefaults, LOCAL_TARGET } from './connection';
|
|
32
|
+
export * from './errors';
|
|
33
|
+
export * from './grpc-retry';
|
|
34
|
+
export * from './interceptors';
|
|
35
|
+
export * from './types';
|
|
36
|
+
export * from './workflow-client';
|
|
37
|
+
export * from './workflow-options';
|