@temporalio/client 1.0.0-rc.0 → 1.0.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.
@@ -0,0 +1,979 @@
1
+ import { status as grpcStatus } from '@grpc/grpc-js';
2
+ import {
3
+ CancelledFailure,
4
+ DataConverter,
5
+ LoadedDataConverter,
6
+ mapFromPayloads,
7
+ mapToPayloads,
8
+ RetryState,
9
+ searchAttributePayloadConverter,
10
+ TerminatedFailure,
11
+ TimeoutFailure,
12
+ TimeoutType,
13
+ } from '@temporalio/common';
14
+ import {
15
+ decodeArrayFromPayloads,
16
+ decodeFromPayloadsAtIndex,
17
+ decodeMapFromPayloads,
18
+ decodeOptionalFailureToOptionalError,
19
+ encodeMapToPayloads,
20
+ encodeToPayloads,
21
+ loadDataConverter,
22
+ } from '@temporalio/internal-non-workflow-common';
23
+ import {
24
+ BaseWorkflowHandle,
25
+ compileRetryPolicy,
26
+ composeInterceptors,
27
+ optionalTsToDate,
28
+ QueryDefinition,
29
+ Replace,
30
+ SearchAttributes,
31
+ SignalDefinition,
32
+ tsToDate,
33
+ WithWorkflowArgs,
34
+ Workflow,
35
+ WorkflowNotFoundError,
36
+ WorkflowResultType,
37
+ } from '@temporalio/internal-workflow-common';
38
+ import { temporal } from '@temporalio/proto';
39
+ import os from 'os';
40
+ import { v4 as uuid4 } from 'uuid';
41
+ import { Connection } from './connection';
42
+ import {
43
+ isServerErrorResponse,
44
+ ServiceError,
45
+ WorkflowContinuedAsNewError,
46
+ WorkflowExecutionAlreadyStartedError,
47
+ WorkflowFailedError,
48
+ } from './errors';
49
+ import {
50
+ WorkflowCancelInput,
51
+ WorkflowClientCallsInterceptor,
52
+ WorkflowClientInterceptors,
53
+ WorkflowDescribeInput,
54
+ WorkflowQueryInput,
55
+ WorkflowSignalInput,
56
+ WorkflowSignalWithStartInput,
57
+ WorkflowStartInput,
58
+ WorkflowTerminateInput,
59
+ } from './interceptors';
60
+ import {
61
+ ConnectionLike,
62
+ DescribeWorkflowExecutionResponse,
63
+ GetWorkflowExecutionHistoryRequest,
64
+ Metadata,
65
+ RequestCancelWorkflowExecutionResponse,
66
+ StartWorkflowExecutionRequest,
67
+ TerminateWorkflowExecutionResponse,
68
+ WorkflowExecution,
69
+ WorkflowExecutionDescription,
70
+ WorkflowExecutionStatusName,
71
+ WorkflowService,
72
+ } from './types';
73
+ import { compileWorkflowOptions, WorkflowOptions, WorkflowSignalWithStartOptions } from './workflow-options';
74
+
75
+ /**
76
+ * A client side handle to a single Workflow instance.
77
+ * It can be used to start, signal, query, wait for completion, terminate and cancel a Workflow execution.
78
+ *
79
+ * Given the following Workflow definition:
80
+ * ```ts
81
+ * export const incrementSignal = defineSignal('increment');
82
+ * export const getValueQuery = defineQuery<number>('getValue');
83
+ * export async function counterWorkflow(initialValue: number): Promise<void>;
84
+ * ```
85
+ *
86
+ * Create a handle for running and interacting with a single Workflow:
87
+ * ```ts
88
+ * const client = new WorkflowClient();
89
+ * // Start the Workflow with initialValue of 2.
90
+ * const handle = await client.start({
91
+ * workflowType: counterWorkflow,
92
+ * args: [2],
93
+ * taskQueue: 'tutorial',
94
+ * });
95
+ * await handle.signal(incrementSignal, 2);
96
+ * await handle.query(getValueQuery); // 4
97
+ * await handle.cancel();
98
+ * await handle.result(); // throws WorkflowExecutionCancelledError
99
+ * ```
100
+ */
101
+ export interface WorkflowHandle<T extends Workflow = Workflow> extends BaseWorkflowHandle<T> {
102
+ /**
103
+ * Query a running or completed Workflow.
104
+ *
105
+ * @param def a query definition as returned from {@link defineQuery} or query name (string)
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * await handle.query(getValueQuery);
110
+ * await handle.query<number, []>('getValue');
111
+ * ```
112
+ */
113
+ query<Ret, Args extends any[] = []>(def: QueryDefinition<Ret, Args> | string, ...args: Args): Promise<Ret>;
114
+
115
+ /**
116
+ * Terminate a running Workflow
117
+ */
118
+ terminate(reason?: string): Promise<TerminateWorkflowExecutionResponse>;
119
+
120
+ /**
121
+ * Cancel a running Workflow
122
+ */
123
+ cancel(): Promise<RequestCancelWorkflowExecutionResponse>;
124
+
125
+ /**
126
+ * Describe the current workflow execution
127
+ */
128
+ describe(): Promise<WorkflowExecutionDescription>;
129
+
130
+ /**
131
+ * Readonly accessor to the underlying WorkflowClient
132
+ */
133
+ readonly client: WorkflowClient;
134
+ }
135
+
136
+ /**
137
+ * This interface is exactly the same as {@link WorkflowHandle} except it
138
+ * includes the `firstExecutionRunId` returned from {@link WorkflowClient.start}.
139
+ */
140
+ export interface WorkflowHandleWithFirstExecutionRunId<T extends Workflow = Workflow> extends WorkflowHandle<T> {
141
+ /**
142
+ * Run Id of the first Execution in the Workflow Execution Chain.
143
+ */
144
+ readonly firstExecutionRunId: string;
145
+ }
146
+
147
+ /**
148
+ * This interface is exactly the same as {@link WorkflowHandle} except it
149
+ * includes the `signaledRunId` returned from `signalWithStart`.
150
+ */
151
+ export interface WorkflowHandleWithSignaledRunId<T extends Workflow = Workflow> extends WorkflowHandle<T> {
152
+ /**
153
+ * The Run Id of the bound Workflow at the time of {@link WorkflowClient.signalWithStart}.
154
+ *
155
+ * Since `signalWithStart` may have signaled an existing Workflow Chain, `signaledRunId` might not be the
156
+ * `firstExecutionRunId`.
157
+ */
158
+ readonly signaledRunId: string;
159
+ }
160
+
161
+ export interface WorkflowClientOptions {
162
+ /**
163
+ * {@link DataConverter} to use for serializing and deserializing payloads
164
+ */
165
+ dataConverter?: DataConverter;
166
+
167
+ /**
168
+ * Used to override and extend default Connection functionality
169
+ *
170
+ * Useful for injecting auth headers and tracing Workflow executions
171
+ */
172
+ interceptors?: WorkflowClientInterceptors;
173
+
174
+ /**
175
+ * Identity to report to the server
176
+ *
177
+ * @default `${process.pid}@${os.hostname()}`
178
+ */
179
+ identity?: string;
180
+
181
+ /**
182
+ * Connection to use to communicate with the server.
183
+ *
184
+ * By default `WorkflowClient` connects to localhost.
185
+ *
186
+ * Connections are expensive to construct and should be reused.
187
+ */
188
+ connection?: ConnectionLike;
189
+
190
+ /**
191
+ * Server namespace
192
+ *
193
+ * @default default
194
+ */
195
+ namespace?: string;
196
+
197
+ /**
198
+ * Should a query be rejected by closed and failed workflows
199
+ *
200
+ * @default QUERY_REJECT_CONDITION_UNSPECIFIED which means that closed and failed workflows are still queryable
201
+ */
202
+ queryRejectCondition?: temporal.api.enums.v1.QueryRejectCondition;
203
+ }
204
+
205
+ export type WorkflowClientOptionsWithDefaults = Replace<
206
+ Required<WorkflowClientOptions>,
207
+ {
208
+ connection?: ConnectionLike;
209
+ }
210
+ >;
211
+ export type LoadedWorkflowClientOptions = WorkflowClientOptionsWithDefaults & {
212
+ loadedDataConverter: LoadedDataConverter;
213
+ };
214
+
215
+ export function defaultWorkflowClientOptions(): WorkflowClientOptionsWithDefaults {
216
+ return {
217
+ dataConverter: {},
218
+ // The equivalent in Java is ManagementFactory.getRuntimeMXBean().getName()
219
+ identity: `${process.pid}@${os.hostname()}`,
220
+ interceptors: {},
221
+ namespace: 'default',
222
+ queryRejectCondition: temporal.api.enums.v1.QueryRejectCondition.QUERY_REJECT_CONDITION_UNSPECIFIED,
223
+ };
224
+ }
225
+
226
+ function assertRequiredWorkflowOptions(opts: WorkflowOptions): void {
227
+ if (!opts.taskQueue) {
228
+ throw new TypeError('Missing WorkflowOptions.taskQueue');
229
+ }
230
+ if (!opts.workflowId) {
231
+ throw new TypeError('Missing WorkflowOptions.workflowId');
232
+ }
233
+ }
234
+
235
+ function ensureArgs<W extends Workflow, T extends WorkflowStartOptions<W>>(
236
+ opts: T
237
+ ): Omit<T, 'args'> & { args: unknown[] } {
238
+ const { args, ...rest } = opts;
239
+ return { args: args ?? [], ...rest };
240
+ }
241
+
242
+ /**
243
+ * Options for getting a result of a Workflow execution.
244
+ */
245
+ export interface WorkflowResultOptions {
246
+ /**
247
+ * If set to true, instructs the client to follow the chain of execution before returning a Workflow's result.
248
+ *
249
+ * Workflow execution is chained if the Workflow has a cron schedule or continues-as-new or configured to retry
250
+ * after failure or timeout.
251
+ *
252
+ * @default true
253
+ */
254
+ followRuns?: boolean;
255
+ }
256
+
257
+ export interface GetWorkflowHandleOptions extends WorkflowResultOptions {
258
+ /**
259
+ * ID of the first execution in the Workflow execution chain.
260
+ *
261
+ * When getting a handle with no `runId`, pass this option to ensure some
262
+ * {@link WorkflowHandle} methods (e.g. `terminate` and `cancel`) don't
263
+ * affect executions from another chain.
264
+ */
265
+ firstExecutionRunId?: string;
266
+ }
267
+
268
+ interface WorkflowHandleOptions extends GetWorkflowHandleOptions {
269
+ workflowId: string;
270
+ runId?: string;
271
+ interceptors: WorkflowClientCallsInterceptor[];
272
+ /**
273
+ * A runId to use for getting the workflow's result.
274
+ *
275
+ * - When creating a handle using `getHandle`, uses the provided runId or firstExecutionRunId
276
+ * - When creating a handle using `start`, uses the returned runId (first in the chain)
277
+ * - When creating a handle using `signalWithStart`, uses the the returned runId
278
+ */
279
+ runIdForResult?: string;
280
+ }
281
+
282
+ /**
283
+ * Options for starting a Workflow
284
+ */
285
+ export type WorkflowStartOptions<T extends Workflow = Workflow> = WithWorkflowArgs<T, WorkflowOptions>;
286
+
287
+ /**
288
+ * Client for starting Workflow executions and creating Workflow handles
289
+ */
290
+ export class WorkflowClient {
291
+ public readonly options: LoadedWorkflowClientOptions;
292
+ public readonly connection: ConnectionLike;
293
+
294
+ constructor(options?: WorkflowClientOptions) {
295
+ this.connection = options?.connection ?? Connection.lazy();
296
+ this.options = {
297
+ ...defaultWorkflowClientOptions(),
298
+ ...options,
299
+ loadedDataConverter: loadDataConverter(options?.dataConverter),
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Raw gRPC access to the Temporal service.
305
+ *
306
+ * **NOTE**: The namespace provided in {@link options} is **not** automatically set on requests made to the service.
307
+ */
308
+ get workflowService(): WorkflowService {
309
+ return this.connection.workflowService;
310
+ }
311
+
312
+ /**
313
+ * Set the deadline for any service requests executed in `fn`'s scope.
314
+ */
315
+ async withDeadline<R>(deadline: number | Date, fn: () => Promise<R>): Promise<R> {
316
+ return await this.connection.withDeadline(deadline, fn);
317
+ }
318
+
319
+ /**
320
+ * Set metadata for any service requests executed in `fn`'s scope.
321
+ *
322
+ * @returns returned value of `fn`
323
+ *
324
+ * @see {@link Connection.withMetadata}
325
+ */
326
+ async withMetadata<R>(metadata: Metadata, fn: () => Promise<R>): Promise<R> {
327
+ return await this.connection.withMetadata(metadata, fn);
328
+ }
329
+
330
+ /**
331
+ * Start a new Workflow execution.
332
+ *
333
+ * @returns the execution's `runId`.
334
+ */
335
+ protected async _start<T extends Workflow>(
336
+ workflowTypeOrFunc: string | T,
337
+ options: WithWorkflowArgs<T, WorkflowOptions>,
338
+ interceptors: WorkflowClientCallsInterceptor[]
339
+ ): Promise<string> {
340
+ const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
341
+ assertRequiredWorkflowOptions(options);
342
+ const compiledOptions = compileWorkflowOptions(ensureArgs(options));
343
+
344
+ const start = composeInterceptors(interceptors, 'start', this._startWorkflowHandler.bind(this));
345
+
346
+ return start({
347
+ options: compiledOptions,
348
+ headers: {},
349
+ workflowType,
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Sends a signal to a running Workflow or starts a new one if not already running and immediately signals it.
355
+ * Useful when you're unsure of the Workflows' run state.
356
+ *
357
+ * @returns the runId of the Workflow
358
+ */
359
+ protected async _signalWithStart<T extends Workflow, SA extends any[]>(
360
+ workflowTypeOrFunc: string | T,
361
+ options: WithWorkflowArgs<T, WorkflowSignalWithStartOptions<SA>>,
362
+ interceptors: WorkflowClientCallsInterceptor[]
363
+ ): Promise<string> {
364
+ const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
365
+ const { signal, signalArgs, ...rest } = options;
366
+ assertRequiredWorkflowOptions(rest);
367
+ const compiledOptions = compileWorkflowOptions(ensureArgs(rest));
368
+
369
+ const signalWithStart = composeInterceptors(
370
+ interceptors,
371
+ 'signalWithStart',
372
+ this._signalWithStartWorkflowHandler.bind(this)
373
+ );
374
+
375
+ return signalWithStart({
376
+ options: compiledOptions,
377
+ headers: {},
378
+ workflowType,
379
+ signalName: typeof signal === 'string' ? signal : signal.name,
380
+ signalArgs,
381
+ });
382
+ }
383
+
384
+ /**
385
+ * Start a new Workflow execution.
386
+ *
387
+ * @returns a WorkflowHandle to the started Workflow
388
+ */
389
+ public async start<T extends Workflow>(
390
+ workflowTypeOrFunc: string | T,
391
+ options: WorkflowStartOptions<T>
392
+ ): Promise<WorkflowHandleWithFirstExecutionRunId<T>> {
393
+ const { workflowId } = options;
394
+ // Cast is needed because it's impossible to deduce the type in this situation
395
+ const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId }));
396
+ const runId = await this._start(workflowTypeOrFunc, { ...options, workflowId }, interceptors);
397
+ // runId is not used in handles created with `start*` calls because these
398
+ // handles should allow interacting with the workflow if it continues as new.
399
+ const handle = this._createWorkflowHandle({
400
+ workflowId,
401
+ runId: undefined,
402
+ firstExecutionRunId: runId,
403
+ runIdForResult: runId,
404
+ interceptors,
405
+ followRuns: options.followRuns ?? true,
406
+ }) as WorkflowHandleWithFirstExecutionRunId<T>; // Cast is safe because we know we add the firstExecutionRunId below
407
+ (handle as any) /* readonly */.firstExecutionRunId = runId;
408
+ return handle;
409
+ }
410
+
411
+ /**
412
+ * Sends a signal to a running Workflow or starts a new one if not already running and immediately signals it.
413
+ * Useful when you're unsure of the Workflows' run state.
414
+ *
415
+ * @returns a WorkflowHandle to the started Workflow
416
+ */
417
+ public async signalWithStart<T extends Workflow, SA extends any[] = []>(
418
+ workflowTypeOrFunc: string | T,
419
+ options: WithWorkflowArgs<T, WorkflowSignalWithStartOptions<SA>>
420
+ ): Promise<WorkflowHandleWithSignaledRunId<T>> {
421
+ const { workflowId } = options;
422
+ const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId }));
423
+ const runId = await this._signalWithStart(workflowTypeOrFunc, options, interceptors);
424
+ // runId is not used in handles created with `start*` calls because these
425
+ // handles should allow interacting with the workflow if it continues as new.
426
+ const handle = this._createWorkflowHandle({
427
+ workflowId,
428
+ runId: undefined,
429
+ firstExecutionRunId: undefined, // We don't know if this runId is first in the chain or not
430
+ runIdForResult: runId,
431
+ interceptors,
432
+ followRuns: options.followRuns ?? true,
433
+ }) as WorkflowHandleWithSignaledRunId<T>; // Cast is safe because we know we add the signaledRunId below
434
+ (handle as any) /* readonly */.signaledRunId = runId;
435
+ return handle;
436
+ }
437
+
438
+ /**
439
+ * Starts a new Workflow execution and awaits its completion.
440
+ *
441
+ * @returns the result of the Workflow execution
442
+ */
443
+ public async execute<T extends Workflow>(
444
+ workflowTypeOrFunc: string | T,
445
+ options: WorkflowStartOptions<T>
446
+ ): Promise<WorkflowResultType<T>> {
447
+ const { workflowId } = options;
448
+ const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId }));
449
+ await this._start(workflowTypeOrFunc, options, interceptors);
450
+ return await this.result(workflowId, undefined, {
451
+ ...options,
452
+ followRuns: options.followRuns ?? true,
453
+ });
454
+ }
455
+
456
+ /**
457
+ * Gets the result of a Workflow execution.
458
+ *
459
+ * Follows the chain of execution in case Workflow continues as new, or has a cron schedule or retry policy.
460
+ */
461
+ public async result<T extends Workflow>(
462
+ workflowId: string,
463
+ runId?: string,
464
+ opts?: WorkflowResultOptions
465
+ ): Promise<WorkflowResultType<T>> {
466
+ const followRuns = opts?.followRuns ?? true;
467
+ const execution: temporal.api.common.v1.IWorkflowExecution = { workflowId, runId };
468
+ const req: GetWorkflowExecutionHistoryRequest = {
469
+ namespace: this.options.namespace,
470
+ execution,
471
+ skipArchival: true,
472
+ waitNewEvent: true,
473
+ historyEventFilterType: temporal.api.enums.v1.HistoryEventFilterType.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT,
474
+ };
475
+ let ev: temporal.api.history.v1.IHistoryEvent;
476
+
477
+ for (;;) {
478
+ let res: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse;
479
+ try {
480
+ res = await this.workflowService.getWorkflowExecutionHistory(req);
481
+ } catch (err) {
482
+ this.rethrowGrpcError(err, { workflowId, runId }, 'Failed to get Workflow execution history');
483
+ }
484
+ if (!res.history) {
485
+ throw new Error('No history returned by service');
486
+ }
487
+ const { events } = res.history;
488
+ if (!events) {
489
+ throw new Error('No events in history returned by service');
490
+ }
491
+ if (events.length === 0) {
492
+ req.nextPageToken = res.nextPageToken;
493
+ continue;
494
+ }
495
+ if (events.length !== 1) {
496
+ throw new Error(`Expected at most 1 close event(s), got: ${events.length}`);
497
+ }
498
+ ev = events[0];
499
+
500
+ if (ev.workflowExecutionCompletedEventAttributes) {
501
+ if (followRuns && ev.workflowExecutionCompletedEventAttributes.newExecutionRunId) {
502
+ execution.runId = ev.workflowExecutionCompletedEventAttributes.newExecutionRunId;
503
+ req.nextPageToken = undefined;
504
+ continue;
505
+ }
506
+ // Note that we can only return one value from our workflow function in JS.
507
+ // Ignore any other payloads in result
508
+ const [result] = await decodeArrayFromPayloads(
509
+ this.options.loadedDataConverter,
510
+ ev.workflowExecutionCompletedEventAttributes.result?.payloads
511
+ );
512
+ return result as any;
513
+ } else if (ev.workflowExecutionFailedEventAttributes) {
514
+ if (followRuns && ev.workflowExecutionFailedEventAttributes.newExecutionRunId) {
515
+ execution.runId = ev.workflowExecutionFailedEventAttributes.newExecutionRunId;
516
+ req.nextPageToken = undefined;
517
+ continue;
518
+ }
519
+ const { failure, retryState } = ev.workflowExecutionFailedEventAttributes;
520
+ throw new WorkflowFailedError(
521
+ 'Workflow execution failed',
522
+ await decodeOptionalFailureToOptionalError(this.options.loadedDataConverter, failure),
523
+ retryState ?? RetryState.RETRY_STATE_UNSPECIFIED
524
+ );
525
+ } else if (ev.workflowExecutionCanceledEventAttributes) {
526
+ const failure = new CancelledFailure(
527
+ 'Workflow canceled',
528
+ await decodeArrayFromPayloads(
529
+ this.options.loadedDataConverter,
530
+ ev.workflowExecutionCanceledEventAttributes.details?.payloads
531
+ )
532
+ );
533
+ failure.stack = '';
534
+ throw new WorkflowFailedError(
535
+ 'Workflow execution cancelled',
536
+ failure,
537
+ RetryState.RETRY_STATE_NON_RETRYABLE_FAILURE
538
+ );
539
+ } else if (ev.workflowExecutionTerminatedEventAttributes) {
540
+ const failure = new TerminatedFailure(
541
+ ev.workflowExecutionTerminatedEventAttributes.reason || 'Workflow execution terminated'
542
+ );
543
+ failure.stack = '';
544
+ throw new WorkflowFailedError(
545
+ ev.workflowExecutionTerminatedEventAttributes.reason || 'Workflow execution terminated',
546
+ failure,
547
+ RetryState.RETRY_STATE_NON_RETRYABLE_FAILURE
548
+ );
549
+ } else if (ev.workflowExecutionTimedOutEventAttributes) {
550
+ if (followRuns && ev.workflowExecutionTimedOutEventAttributes.newExecutionRunId) {
551
+ execution.runId = ev.workflowExecutionTimedOutEventAttributes.newExecutionRunId;
552
+ req.nextPageToken = undefined;
553
+ continue;
554
+ }
555
+ const failure = new TimeoutFailure(
556
+ 'Workflow execution timed out',
557
+ undefined,
558
+ TimeoutType.TIMEOUT_TYPE_START_TO_CLOSE
559
+ );
560
+ failure.stack = '';
561
+ throw new WorkflowFailedError(
562
+ 'Workflow execution timed out',
563
+ failure,
564
+ ev.workflowExecutionTimedOutEventAttributes.retryState || 0
565
+ );
566
+ } else if (ev.workflowExecutionContinuedAsNewEventAttributes) {
567
+ const { newExecutionRunId } = ev.workflowExecutionContinuedAsNewEventAttributes;
568
+ if (!newExecutionRunId) {
569
+ throw new TypeError('Expected service to return newExecutionRunId for WorkflowExecutionContinuedAsNewEvent');
570
+ }
571
+ if (!followRuns) {
572
+ throw new WorkflowContinuedAsNewError('Workflow execution continued as new', newExecutionRunId);
573
+ }
574
+ execution.runId = newExecutionRunId;
575
+ req.nextPageToken = undefined;
576
+ continue;
577
+ }
578
+ }
579
+ }
580
+
581
+ protected rethrowGrpcError(err: unknown, workflowExecution: WorkflowExecution, fallbackMessage: string): never {
582
+ if (isServerErrorResponse(err)) {
583
+ if (err.code === grpcStatus.NOT_FOUND) {
584
+ throw new WorkflowNotFoundError(
585
+ err.details ?? 'Workflow not found',
586
+ workflowExecution.workflowId,
587
+ workflowExecution.runId
588
+ );
589
+ }
590
+ throw new ServiceError(fallbackMessage, { cause: err });
591
+ }
592
+ throw new ServiceError('Unexpected error while making gRPC request');
593
+ }
594
+
595
+ /**
596
+ * Uses given input to make a queryWorkflow call to the service
597
+ *
598
+ * Used as the final function of the query interceptor chain
599
+ */
600
+ protected async _queryWorkflowHandler(input: WorkflowQueryInput): Promise<unknown> {
601
+ let response: temporal.api.workflowservice.v1.QueryWorkflowResponse;
602
+ try {
603
+ response = await this.workflowService.queryWorkflow({
604
+ queryRejectCondition: input.queryRejectCondition,
605
+ namespace: this.options.namespace,
606
+ execution: input.workflowExecution,
607
+ query: {
608
+ queryType: input.queryType,
609
+ queryArgs: { payloads: await encodeToPayloads(this.options.loadedDataConverter, ...input.args) },
610
+ header: { fields: input.headers },
611
+ },
612
+ });
613
+ } catch (err) {
614
+ if (isServerErrorResponse(err) && err.code === grpcStatus.INVALID_ARGUMENT) {
615
+ throw new QueryNotRegisteredError(err.message.replace(/^3 INVALID_ARGUMENT: /, ''), err.code);
616
+ }
617
+ this.rethrowGrpcError(err, input.workflowExecution, 'Failed to query Workflow');
618
+ }
619
+ if (response.queryRejected) {
620
+ if (response.queryRejected.status === undefined || response.queryRejected.status === null) {
621
+ throw new TypeError('Received queryRejected from server with no status');
622
+ }
623
+ throw new QueryRejectedError(response.queryRejected.status);
624
+ }
625
+ if (!response.queryResult) {
626
+ throw new TypeError('Invalid response from server');
627
+ }
628
+ // We ignore anything but the first result
629
+ return await decodeFromPayloadsAtIndex(this.options.loadedDataConverter, 0, response.queryResult?.payloads);
630
+ }
631
+
632
+ /**
633
+ * Uses given input to make a signalWorkflowExecution call to the service
634
+ *
635
+ * Used as the final function of the signal interceptor chain
636
+ */
637
+ protected async _signalWorkflowHandler(input: WorkflowSignalInput): Promise<void> {
638
+ try {
639
+ await this.workflowService.signalWorkflowExecution({
640
+ identity: this.options.identity,
641
+ namespace: this.options.namespace,
642
+ workflowExecution: input.workflowExecution,
643
+ requestId: uuid4(),
644
+ // control is unused,
645
+ signalName: input.signalName,
646
+ header: { fields: input.headers },
647
+ input: { payloads: await encodeToPayloads(this.options.loadedDataConverter, ...input.args) },
648
+ });
649
+ } catch (err) {
650
+ this.rethrowGrpcError(err, input.workflowExecution, 'Failed to signal Workflow');
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Uses given input to make a signalWithStartWorkflowExecution call to the service
656
+ *
657
+ * Used as the final function of the signalWithStart interceptor chain
658
+ */
659
+ protected async _signalWithStartWorkflowHandler(input: WorkflowSignalWithStartInput): Promise<string> {
660
+ const { identity } = this.options;
661
+ const { options, workflowType, signalName, signalArgs, headers } = input;
662
+ try {
663
+ const { runId } = await this.workflowService.signalWithStartWorkflowExecution({
664
+ namespace: this.options.namespace,
665
+ identity,
666
+ requestId: uuid4(),
667
+ workflowId: options.workflowId,
668
+ workflowIdReusePolicy: options.workflowIdReusePolicy,
669
+ workflowType: { name: workflowType },
670
+ input: { payloads: await encodeToPayloads(this.options.loadedDataConverter, ...options.args) },
671
+ signalName,
672
+ signalInput: { payloads: await encodeToPayloads(this.options.loadedDataConverter, ...signalArgs) },
673
+ taskQueue: {
674
+ kind: temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_UNSPECIFIED,
675
+ name: options.taskQueue,
676
+ },
677
+ workflowExecutionTimeout: options.workflowExecutionTimeout,
678
+ workflowRunTimeout: options.workflowRunTimeout,
679
+ workflowTaskTimeout: options.workflowTaskTimeout,
680
+ retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
681
+ memo: options.memo
682
+ ? { fields: await encodeMapToPayloads(this.options.loadedDataConverter, options.memo) }
683
+ : undefined,
684
+ searchAttributes: options.searchAttributes
685
+ ? {
686
+ indexedFields: mapToPayloads(searchAttributePayloadConverter, options.searchAttributes),
687
+ }
688
+ : undefined,
689
+ cronSchedule: options.cronSchedule,
690
+ header: { fields: headers },
691
+ });
692
+ return runId;
693
+ } catch (err) {
694
+ this.rethrowGrpcError(err, { workflowId: options.workflowId }, 'Failed to signalWithStart Workflow');
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Uses given input to make startWorkflowExecution call to the service
700
+ *
701
+ * Used as the final function of the start interceptor chain
702
+ */
703
+ protected async _startWorkflowHandler(input: WorkflowStartInput): Promise<string> {
704
+ const { options: opts, workflowType, headers } = input;
705
+ const { identity } = this.options;
706
+ const req: StartWorkflowExecutionRequest = {
707
+ namespace: this.options.namespace,
708
+ identity,
709
+ requestId: uuid4(),
710
+ workflowId: opts.workflowId,
711
+ workflowIdReusePolicy: opts.workflowIdReusePolicy,
712
+ workflowType: { name: workflowType },
713
+ input: { payloads: await encodeToPayloads(this.options.loadedDataConverter, ...opts.args) },
714
+ taskQueue: {
715
+ kind: temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_UNSPECIFIED,
716
+ name: opts.taskQueue,
717
+ },
718
+ workflowExecutionTimeout: opts.workflowExecutionTimeout,
719
+ workflowRunTimeout: opts.workflowRunTimeout,
720
+ workflowTaskTimeout: opts.workflowTaskTimeout,
721
+ retryPolicy: opts.retry ? compileRetryPolicy(opts.retry) : undefined,
722
+ memo: opts.memo ? { fields: await encodeMapToPayloads(this.options.loadedDataConverter, opts.memo) } : undefined,
723
+ searchAttributes: opts.searchAttributes
724
+ ? {
725
+ indexedFields: mapToPayloads(searchAttributePayloadConverter, opts.searchAttributes),
726
+ }
727
+ : undefined,
728
+ cronSchedule: opts.cronSchedule,
729
+ header: { fields: headers },
730
+ };
731
+ try {
732
+ const res = await this.workflowService.startWorkflowExecution(req);
733
+ return res.runId;
734
+ } catch (err: any) {
735
+ if (err.code === grpcStatus.ALREADY_EXISTS) {
736
+ throw new WorkflowExecutionAlreadyStartedError(
737
+ 'Workflow execution already started',
738
+ opts.workflowId,
739
+ workflowType
740
+ );
741
+ }
742
+ this.rethrowGrpcError(err, { workflowId: opts.workflowId }, 'Failed to start Workflow');
743
+ }
744
+ }
745
+
746
+ /**
747
+ * Uses given input to make terminateWorkflowExecution call to the service
748
+ *
749
+ * Used as the final function of the terminate interceptor chain
750
+ */
751
+ protected async _terminateWorkflowHandler(
752
+ input: WorkflowTerminateInput
753
+ ): Promise<TerminateWorkflowExecutionResponse> {
754
+ try {
755
+ return await this.workflowService.terminateWorkflowExecution({
756
+ namespace: this.options.namespace,
757
+ identity: this.options.identity,
758
+ ...input,
759
+ details: {
760
+ payloads: input.details
761
+ ? await encodeToPayloads(this.options.loadedDataConverter, ...input.details)
762
+ : undefined,
763
+ },
764
+ firstExecutionRunId: input.firstExecutionRunId,
765
+ });
766
+ } catch (err) {
767
+ this.rethrowGrpcError(err, input.workflowExecution, 'Failed to terminate Workflow');
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Uses given input to make requestCancelWorkflowExecution call to the service
773
+ *
774
+ * Used as the final function of the cancel interceptor chain
775
+ */
776
+ protected async _cancelWorkflowHandler(input: WorkflowCancelInput): Promise<RequestCancelWorkflowExecutionResponse> {
777
+ try {
778
+ return await this.workflowService.requestCancelWorkflowExecution({
779
+ namespace: this.options.namespace,
780
+ identity: this.options.identity,
781
+ requestId: uuid4(),
782
+ workflowExecution: input.workflowExecution,
783
+ firstExecutionRunId: input.firstExecutionRunId,
784
+ });
785
+ } catch (err) {
786
+ this.rethrowGrpcError(err, input.workflowExecution, 'Failed to cancel workflow');
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Uses given input to make describeWorkflowExecution call to the service
792
+ *
793
+ * Used as the final function of the describe interceptor chain
794
+ */
795
+ protected async _describeWorkflowHandler(input: WorkflowDescribeInput): Promise<DescribeWorkflowExecutionResponse> {
796
+ try {
797
+ return await this.workflowService.describeWorkflowExecution({
798
+ namespace: this.options.namespace,
799
+ execution: input.workflowExecution,
800
+ });
801
+ } catch (err) {
802
+ this.rethrowGrpcError(err, input.workflowExecution, 'Failed to describe workflow');
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Create a new workflow handle for new or existing Workflow execution
808
+ */
809
+ protected _createWorkflowHandle<T extends Workflow>({
810
+ workflowId,
811
+ runId,
812
+ firstExecutionRunId,
813
+ interceptors,
814
+ runIdForResult,
815
+ ...resultOptions
816
+ }: WorkflowHandleOptions): WorkflowHandle<T> {
817
+ return {
818
+ client: this,
819
+ workflowId,
820
+ async result(): Promise<WorkflowResultType<T>> {
821
+ return this.client.result(workflowId, runIdForResult, resultOptions);
822
+ },
823
+ async terminate(reason?: string) {
824
+ const next = this.client._terminateWorkflowHandler.bind(this.client);
825
+ const fn = interceptors.length ? composeInterceptors(interceptors, 'terminate', next) : next;
826
+ return await fn({
827
+ workflowExecution: { workflowId, runId },
828
+ reason,
829
+ firstExecutionRunId,
830
+ });
831
+ },
832
+ async cancel() {
833
+ const next = this.client._cancelWorkflowHandler.bind(this.client);
834
+ const fn = interceptors.length ? composeInterceptors(interceptors, 'cancel', next) : next;
835
+ return await fn({
836
+ workflowExecution: { workflowId, runId },
837
+ firstExecutionRunId,
838
+ });
839
+ },
840
+ async describe() {
841
+ const next = this.client._describeWorkflowHandler.bind(this.client);
842
+ const fn = interceptors.length ? composeInterceptors(interceptors, 'describe', next) : next;
843
+ const raw = await fn({
844
+ workflowExecution: { workflowId, runId },
845
+ });
846
+ return {
847
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
848
+ type: raw.workflowExecutionInfo!.type!.name!,
849
+ workflowId: raw.workflowExecutionInfo!.execution!.workflowId!,
850
+ runId: raw.workflowExecutionInfo!.execution!.runId!,
851
+ taskQueue: raw.workflowExecutionInfo!.taskQueue!,
852
+ status: {
853
+ code: raw.workflowExecutionInfo!.status!,
854
+ name: workflowStatusCodeToName(raw.workflowExecutionInfo!.status!),
855
+ },
856
+ historyLength: raw.workflowExecutionInfo!.historyLength!,
857
+ startTime: tsToDate(raw.workflowExecutionInfo!.startTime!),
858
+ executionTime: optionalTsToDate(raw.workflowExecutionInfo!.executionTime),
859
+ closeTime: optionalTsToDate(raw.workflowExecutionInfo!.closeTime),
860
+ memo: await decodeMapFromPayloads(
861
+ this.client.options.loadedDataConverter,
862
+ raw.workflowExecutionInfo!.memo?.fields
863
+ ),
864
+ searchAttributes: mapFromPayloads(
865
+ searchAttributePayloadConverter,
866
+ raw.workflowExecutionInfo!.searchAttributes?.indexedFields ?? {}
867
+ ) as SearchAttributes,
868
+ parentExecution: raw.workflowExecutionInfo?.parentExecution
869
+ ? {
870
+ workflowId: raw.workflowExecutionInfo.parentExecution.workflowId!,
871
+ runId: raw.workflowExecutionInfo.parentExecution.runId!,
872
+ }
873
+ : undefined,
874
+ raw,
875
+ };
876
+ },
877
+ async signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
878
+ const next = this.client._signalWorkflowHandler.bind(this.client);
879
+ const fn = interceptors.length ? composeInterceptors(interceptors, 'signal', next) : next;
880
+ await fn({
881
+ workflowExecution: { workflowId, runId },
882
+ signalName: typeof def === 'string' ? def : def.name,
883
+ args,
884
+ headers: {},
885
+ });
886
+ },
887
+ async query<Ret, Args extends any[]>(def: QueryDefinition<Ret, Args> | string, ...args: Args): Promise<Ret> {
888
+ const next = this.client._queryWorkflowHandler.bind(this.client);
889
+ const fn = interceptors.length ? composeInterceptors(interceptors, 'query', next) : next;
890
+ return fn({
891
+ workflowExecution: { workflowId, runId },
892
+ queryRejectCondition: this.client.options.queryRejectCondition,
893
+ queryType: typeof def === 'string' ? def : def.name,
894
+ args,
895
+ headers: {},
896
+ }) as Promise<Ret>;
897
+ },
898
+ };
899
+ }
900
+
901
+ /**
902
+ * Create a handle to an existing Workflow.
903
+ *
904
+ * - If only `workflowId` is passed, and there are multiple Workflow Executions with that ID, the handle will refer to
905
+ * the most recent one.
906
+ * - If `workflowId` and `runId` are passed, the handle will refer to the specific Workflow Execution with that Run
907
+ * ID.
908
+ * - If `workflowId` and {@link GetWorkflowHandleOptions.firstExecutionRunId} are passed, the handle will refer to the
909
+ * most recent Workflow Execution in the *Chain* that started with `firstExecutionRunId`.
910
+ *
911
+ * A *Chain* is a series of Workflow Executions that share the same Workflow ID and are connected by:
912
+ * - Being part of the same {@link https://docs.temporal.io/typescript/clients#scheduling-cron-workflows | Cron}
913
+ * - {@link https://docs.temporal.io/typescript/workflows#continueasnew | Continue As New}
914
+ * - {@link https://typescript.temporal.io/api/interfaces/client.workflowoptions/#retry | Retries}
915
+ *
916
+ * This method does not validate `workflowId`. If there is no Workflow Execution with the given `workflowId`, handle
917
+ * methods like `handle.describe()` will throw a {@link WorkflowNotFoundError} error.
918
+ */
919
+ public getHandle<T extends Workflow>(
920
+ workflowId: string,
921
+ runId?: string,
922
+ options?: GetWorkflowHandleOptions
923
+ ): WorkflowHandle<T> {
924
+ const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId, runId }));
925
+
926
+ return this._createWorkflowHandle({
927
+ workflowId,
928
+ runId,
929
+ firstExecutionRunId: options?.firstExecutionRunId,
930
+ runIdForResult: runId ?? options?.firstExecutionRunId,
931
+ interceptors,
932
+ followRuns: options?.followRuns ?? true,
933
+ });
934
+ }
935
+ }
936
+
937
+ export class QueryRejectedError extends Error {
938
+ public readonly name: string = 'QueryRejectedError';
939
+ constructor(public readonly status: temporal.api.enums.v1.WorkflowExecutionStatus) {
940
+ super('Query rejected');
941
+ }
942
+ }
943
+
944
+ export class QueryNotRegisteredError extends Error {
945
+ public readonly name: string = 'QueryNotRegisteredError';
946
+ constructor(message: string, public readonly code: grpcStatus) {
947
+ super(message);
948
+ }
949
+ }
950
+
951
+ function workflowStatusCodeToName(code: temporal.api.enums.v1.WorkflowExecutionStatus): WorkflowExecutionStatusName {
952
+ return workflowStatusCodeToNameInternal(code) ?? 'UNKNOWN';
953
+ }
954
+
955
+ /**
956
+ * Intentionally leave out `default` branch to get compilation errors when new values are added
957
+ */
958
+ function workflowStatusCodeToNameInternal(
959
+ code: temporal.api.enums.v1.WorkflowExecutionStatus
960
+ ): WorkflowExecutionStatusName {
961
+ switch (code) {
962
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_UNSPECIFIED:
963
+ return 'UNSPECIFIED';
964
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING:
965
+ return 'RUNNING';
966
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED:
967
+ return 'FAILED';
968
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT:
969
+ return 'TIMED_OUT';
970
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED:
971
+ return 'CANCELLED';
972
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED:
973
+ return 'TERMINATED';
974
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED:
975
+ return 'COMPLETED';
976
+ case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW:
977
+ return 'CONTINUED_AS_NEW';
978
+ }
979
+ }