Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 917be76

Browse files
authored
feat!(client): Make workflowId required (temporalio#387)
This also removes the `WorkflowClientOptions.workflowDefaults`. Reasoning: - Workflow IDs should represent a meaningful business ID - Workflow IDs can be used as an idempotency key when starting workflows from an external signal - `workflowDefaults` were removed because their presence made `taskQueue` optional in `WorkflowOptions`, omitting it from both the defaults and options resulted in runtime errors where we could have caught those at compile time.\ Migration: ```ts // Before const client = new WorkflowClient(conn.service, { workflowDefaults: { taskQueue: 'example' } }); const handle = await client.start(myWorkflow, { args: [foo, bar] }); // After const client = new WorkflowClient(conn.service); const handle = await client.start(myWorkflow, { args: [foo, bar], taskQueue: 'example', workflowId: 'a-meaningful-business-id', }); ```
1 parent 409d015 commit 917be76

15 files changed

+277
-183
lines changed

packages/client/src/workflow-client.ts

Lines changed: 27 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,13 @@ import {
1515
SignalDefinition,
1616
WorkflowResultType,
1717
WithWorkflowArgs,
18-
WorkflowReturnType,
1918
CancelledFailure,
2019
TerminatedFailure,
2120
RetryState,
2221
TimeoutFailure,
2322
TimeoutType,
2423
} from '@temporalio/common';
25-
import {
26-
WorkflowOptions,
27-
addDefaults,
28-
compileWorkflowOptions,
29-
WorkflowSignalWithStartOptions,
30-
} from './workflow-options';
24+
import { WorkflowOptions, compileWorkflowOptions, WorkflowSignalWithStartOptions } from './workflow-options';
3125
import {
3226
WorkflowCancelInput,
3327
WorkflowClientCallsInterceptor,
@@ -152,14 +146,6 @@ export interface WorkflowClientOptions {
152146
* @default QUERY_REJECT_CONDITION_UNSPECIFIED which means that closed and failed workflows are still queryable
153147
*/
154148
queryRejectCondition?: temporal.api.enums.v1.QueryRejectCondition;
155-
156-
/**
157-
* Apply default options for starting new Workflows.
158-
*
159-
* These defaults are **shallowly** merged with options provided to methods that start Workflows
160-
* e.g. {@link WorkflowClient.start}.
161-
*/
162-
workflowDefaults?: Partial<WorkflowOptions>;
163149
}
164150

165151
export type WorkflowClientOptionsWithDefaults = Required<WorkflowClientOptions>;
@@ -172,14 +158,23 @@ export function defaultWorkflowClientOptions(): WorkflowClientOptionsWithDefault
172158
interceptors: {},
173159
namespace: 'default',
174160
queryRejectCondition: temporal.api.enums.v1.QueryRejectCondition.QUERY_REJECT_CONDITION_UNSPECIFIED,
175-
workflowDefaults: {},
176161
};
177162
}
178163

179-
function assertRequiredWorkflowOptions(opts: Partial<WorkflowOptions>): asserts opts is WorkflowOptions {
164+
function assertRequiredWorkflowOptions(opts: WorkflowOptions): void {
180165
if (!opts.taskQueue) {
181166
throw new TypeError('Missing WorkflowOptions.taskQueue');
182167
}
168+
if (!opts.workflowId) {
169+
throw new TypeError('Missing WorkflowOptions.workflowId');
170+
}
171+
}
172+
173+
function ensureArgs<W extends Workflow, T extends WorkflowStartOptions<W>>(
174+
opts: T
175+
): Omit<T, 'args'> & { args: unknown[] } {
176+
const { args, ...rest } = opts;
177+
return { args: args ?? [], ...rest };
183178
}
184179

185180
/**
@@ -200,7 +195,7 @@ export interface WorkflowResultOptions {
200195
/**
201196
* Options for starting a Workflow
202197
*/
203-
export type WorkflowStartOptions<T extends Workflow> = WithWorkflowArgs<T, Partial<WorkflowOptions>>;
198+
export type WorkflowStartOptions<T extends Workflow> = WithWorkflowArgs<T, WorkflowOptions>;
204199

205200
/**
206201
* Client for starting Workflow executions and creating Workflow handles
@@ -219,12 +214,12 @@ export class WorkflowClient {
219214
*/
220215
protected async _start<T extends Workflow>(
221216
workflowTypeOrFunc: string | T,
222-
options: WithWorkflowArgs<T, Partial<WorkflowOptions>>,
217+
options: WithWorkflowArgs<T, WorkflowOptions>,
223218
interceptors: WorkflowClientCallsInterceptor[]
224219
): Promise<string> {
225220
const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
226221
assertRequiredWorkflowOptions(options);
227-
const compiledOptions = compileWorkflowOptions(addDefaults(options));
222+
const compiledOptions = compileWorkflowOptions(ensureArgs(options));
228223

229224
const start = composeInterceptors(interceptors, 'start', this._startWorkflowHandler.bind(this));
230225

@@ -249,7 +244,7 @@ export class WorkflowClient {
249244
const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
250245
const { signal, signalArgs, ...rest } = options;
251246
assertRequiredWorkflowOptions(rest);
252-
const compiledOptions = compileWorkflowOptions(addDefaults(rest));
247+
const compiledOptions = compileWorkflowOptions(ensureArgs(rest));
253248

254249
const signalWithStart = composeInterceptors(
255250
interceptors,
@@ -266,17 +261,6 @@ export class WorkflowClient {
266261
});
267262
}
268263

269-
/**
270-
* Start a new Workflow execution.
271-
*
272-
* **Override for Workflows that accept no arguments**.
273-
*
274-
* @returns a WorkflowHandle to the started Workflow
275-
*/
276-
public async start<T extends () => WorkflowReturnType>(
277-
workflowTypeOrFunc: string | T
278-
): Promise<WorkflowHandleWithRunId<T>>;
279-
280264
/**
281265
* Start a new Workflow execution.
282266
*
@@ -285,15 +269,9 @@ export class WorkflowClient {
285269
public async start<T extends Workflow>(
286270
workflowTypeOrFunc: string | T,
287271
options: WorkflowStartOptions<T>
288-
): Promise<WorkflowHandleWithRunId<T>>;
289-
290-
public async start<T extends Workflow>(
291-
workflowTypeOrFunc: string | T,
292-
maybeOptions?: WorkflowStartOptions<T>
293272
): Promise<WorkflowHandleWithRunId<T>> {
273+
const { workflowId } = options;
294274
// Cast is needed because it's impossible to deduce the type in this situation
295-
const options = { ...this.options.workflowDefaults, ...maybeOptions } as WorkflowStartOptions<T>;
296-
const workflowId = options.workflowId ?? uuid4();
297275
const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId }));
298276
const runId = await this._start(workflowTypeOrFunc, { ...options, workflowId }, interceptors);
299277
const handle = this._createWorkflowHandle(workflowId, runId, interceptors, {
@@ -313,10 +291,9 @@ export class WorkflowClient {
313291
workflowTypeOrFunc: string | T,
314292
options: WithWorkflowArgs<T, WorkflowSignalWithStartOptions<SA>>
315293
): Promise<WorkflowHandleWithRunId<T>> {
316-
options = { ...this.options.workflowDefaults, ...options };
317-
const workflowId = options.workflowId ?? uuid4();
294+
const { workflowId } = options;
318295
const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId }));
319-
const runId = await this._signalWithStart(workflowTypeOrFunc, { ...options, workflowId }, interceptors);
296+
const runId = await this._signalWithStart(workflowTypeOrFunc, options, interceptors);
320297
const handle = this._createWorkflowHandle(workflowId, runId, interceptors, {
321298
followRuns: options.followRuns ?? true,
322299
}) as WorkflowHandleWithRunId<T>; // Cast is safe because we know we add the originalRunId below
@@ -332,28 +309,10 @@ export class WorkflowClient {
332309
public async execute<T extends Workflow>(
333310
workflowTypeOrFunc: string | T,
334311
options: WorkflowStartOptions<T>
335-
): Promise<WorkflowResultType<T>>;
336-
337-
/**
338-
* Starts a new Workflow execution and awaits its completion.
339-
*
340-
* **Override for Workflows that accept no arguments**.
341-
*
342-
* @returns the result of the Workflow execution
343-
*/
344-
public async execute<T extends () => WorkflowReturnType>(
345-
workflowTypeOrFunc: string | T
346-
): Promise<WorkflowResultType<T>>;
347-
348-
public async execute<T extends Workflow>(
349-
workflowTypeOrFunc: string | T,
350-
maybeOptions?: WorkflowStartOptions<T>
351312
): Promise<WorkflowResultType<T>> {
352-
// Cast is needed because it's impossible to deduce the type in this situation
353-
const options = { ...this.options.workflowDefaults, ...maybeOptions } as WorkflowStartOptions<T>;
354-
const workflowId = options.workflowId ?? uuid4();
313+
const { workflowId } = options;
355314
const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId }));
356-
await this._start(workflowTypeOrFunc, { ...options, workflowId }, interceptors);
315+
await this._start(workflowTypeOrFunc, options, interceptors);
357316
return await this.result(workflowId, undefined, {
358317
followRuns: options.followRuns ?? true,
359318
});
@@ -695,11 +654,15 @@ export class WorkflowClient {
695654
/**
696655
* Creates a Workflow handle for existing Workflow using `workflowId` and optional `runId`
697656
*/
698-
public getHandle<T extends Workflow>(workflowId: string, runId?: string): WorkflowHandle<T> {
657+
public getHandle<T extends Workflow>(
658+
workflowId: string,
659+
runId?: string,
660+
options?: WorkflowResultOptions
661+
): WorkflowHandle<T> {
699662
const interceptors = (this.options.interceptors.calls ?? []).map((ctor) => ctor({ workflowId, runId }));
700663

701664
return this._createWorkflowHandle(workflowId, runId, interceptors, {
702-
followRuns: this.options.workflowDefaults.followRuns ?? true,
665+
followRuns: options?.followRuns ?? true,
703666
});
704667
}
705668
}
Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
1-
import { v4 as uuid4 } from 'uuid';
2-
import {
3-
WithWorkflowArgs,
4-
WorkflowOptions as CommonWorkflowOptions,
5-
WorkflowOptionsWithDefaults as CommonWorkflowOptionsWithDefaults,
6-
SignalDefinition,
7-
Workflow,
8-
} from '@temporalio/common';
1+
import { WithCompiledWorkflowDurationOptions, CommonWorkflowOptions, SignalDefinition } from '@temporalio/common';
92

10-
export { CompiledWorkflowOptions, compileWorkflowOptions } from '@temporalio/common';
3+
export { WithCompiledWorkflowDurationOptions, compileWorkflowOptions } from '@temporalio/common';
4+
5+
export interface CompiledWorkflowOptions extends WithCompiledWorkflowDurationOptions<WorkflowOptions> {
6+
args: unknown[];
7+
}
118

129
export interface WorkflowOptions extends CommonWorkflowOptions {
10+
/**
11+
* Workflow id to use when starting.
12+
*
13+
* Assign a meaningful business id.
14+
* This ID can be used to ensure starting Workflows is idempotent.
15+
* Workflow IDs are unique, see also {@link WorkflowOptions.workflowIdReusePolicy}
16+
*/
17+
workflowId: string;
18+
1319
/**
1420
* Task queue to use for Workflow tasks. It should match a task queue specified when creating a
1521
* `Worker` that hosts the Workflow code.
1622
*/
1723
taskQueue: string;
1824

25+
/**
26+
* If set to true, instructs the client to follow the chain of execution before returning a Workflow's result.
27+
*
28+
* Workflow execution is chained if the Workflow has a cron schedule or continues-as-new or configured to retry
29+
* after failure or timeout.
30+
*
31+
* @default true
32+
*/
1933
followRuns?: boolean;
2034
}
2135

22-
export interface WorkflowSignalWithStartOptions<SA extends any[] = []> extends Partial<WorkflowOptions> {
36+
export interface WorkflowSignalWithStartOptions<SA extends any[] = []> extends WorkflowOptions {
2337
/**
2438
* SignalDefinition or name of signal
2539
*/
@@ -30,29 +44,29 @@ export interface WorkflowSignalWithStartOptions<SA extends any[] = []> extends P
3044
signalArgs: SA;
3145
}
3246

33-
export interface WorkflowOptionsWithDefaults<T extends Workflow> extends CommonWorkflowOptionsWithDefaults<T> {
34-
/**
35-
* If set to true, instructs the client to follow the chain of execution before returning a Workflow's result.
36-
*
37-
* Workflow execution is chained if the Workflow has a cron schedule or continues-as-new or configured to retry
38-
* after failure or timeout.
39-
*
40-
* @default true
41-
*/
42-
followRuns: boolean;
43-
}
44-
45-
/**
46-
* Adds default values to `workflowId` and `workflowIdReusePolicy` to given workflow options.
47-
*/
48-
export function addDefaults<T extends Workflow>(
49-
opts: WithWorkflowArgs<T, WorkflowOptions>
50-
): WorkflowOptionsWithDefaults<T> {
51-
const { workflowId, args, ...rest } = opts;
52-
return {
53-
followRuns: true,
54-
args: args ?? [],
55-
workflowId: workflowId ?? uuid4(),
56-
...rest,
57-
};
58-
}
47+
// export interface WorkflowOptionsWithDefaults<T extends Workflow> extends CommonWorkflowOptionsWithDefaults<T> {
48+
// /**
49+
// * If set to true, instructs the client to follow the chain of execution before returning a Workflow's result.
50+
// *
51+
// * Workflow execution is chained if the Workflow has a cron schedule or continues-as-new or configured to retry
52+
// * after failure or timeout.
53+
// *
54+
// * @default true
55+
// */
56+
// followRuns: boolean;
57+
// }
58+
//
59+
// /**
60+
// * Adds default values to `workflowId` and `workflowIdReusePolicy` to given workflow options.
61+
// */
62+
// export function addDefaults<T extends Workflow>(
63+
// opts: WithWorkflowArgs<T, WorkflowOptions>
64+
// ): WorkflowOptionsWithDefaults<T> {
65+
// const { workflowId, args, ...rest } = opts;
66+
// return {
67+
// followRuns: true,
68+
// args: args ?? [],
69+
// workflowId: workflowId ?? uuid4(),
70+
// ...rest,
71+
// };
72+
// }

packages/common/src/workflow-options.ts

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@ export const WorkflowIdReusePolicy = coresdk.common.WorkflowIdReusePolicy;
77
export type RetryPolicy = coresdk.common.IRetryPolicy;
88

99
export interface BaseWorkflowOptions {
10-
/**
11-
* Workflow id to use when starting. If not specified a UUID is generated. Note that it is
12-
* dangerous as in case of client side retries no deduplication will happen based on the
13-
* generated id. So prefer assigning business meaningful ids if possible.
14-
*/
15-
workflowId?: string;
16-
1710
/**
1811
* Specifies server behavior if a completed workflow with the same id exists. Note that under no
1912
* conditions Temporal allows two workflows with the same namespace and workflow id run
@@ -25,12 +18,6 @@ export interface BaseWorkflowOptions {
2518
*/
2619
workflowIdReusePolicy?: WorkflowIdReusePolicy;
2720

28-
/**
29-
* Task queue to use for Workflow tasks. It should match a task queue specified when creating a
30-
* `Worker` that hosts the Workflow code.
31-
*/
32-
taskQueue?: string;
33-
3421
retryPolicy?: coresdk.common.IRetryPolicy;
3522

3623
/**
@@ -100,29 +87,23 @@ export interface WorkflowDurationOptions {
10087
workflowTaskTimeout?: string | number;
10188
}
10289

103-
export type WorkflowOptions = BaseWorkflowOptions & WorkflowDurationOptions;
90+
export type CommonWorkflowOptions = BaseWorkflowOptions & WorkflowDurationOptions;
10491

105-
export type RequiredWorkflowOptions<T extends Workflow = Workflow> = Required<
106-
Pick<BaseWorkflowOptions, 'workflowId' | 'taskQueue'>
92+
export type WithCompiledWorkflowDurationOptions<T extends WorkflowDurationOptions> = Omit<
93+
T,
94+
'workflowExecutionTimeout' | 'workflowRunTimeout' | 'workflowTaskTimeout'
10795
> & {
108-
args: Parameters<T>[];
96+
workflowExecutionTimeout?: google.protobuf.IDuration;
97+
workflowRunTimeout?: google.protobuf.IDuration;
98+
workflowTaskTimeout?: google.protobuf.IDuration;
10999
};
110100

111-
export type WorkflowOptionsWithDefaults<T extends Workflow = Workflow> = WorkflowOptions & RequiredWorkflowOptions<T>;
112-
113-
export type CompiledWorkflowOptions<T extends Workflow = Workflow> = BaseWorkflowOptions &
114-
RequiredWorkflowOptions<T> & {
115-
workflowExecutionTimeout?: google.protobuf.IDuration;
116-
workflowRunTimeout?: google.protobuf.IDuration;
117-
workflowTaskTimeout?: google.protobuf.IDuration;
118-
};
119-
120-
export function compileWorkflowOptions<T extends Workflow>({
101+
export function compileWorkflowOptions<T extends WorkflowDurationOptions>({
121102
workflowExecutionTimeout,
122103
workflowRunTimeout,
123104
workflowTaskTimeout,
124105
...rest
125-
}: WorkflowOptionsWithDefaults<T>): CompiledWorkflowOptions<T> {
106+
}: T): WithCompiledWorkflowDurationOptions<T> {
126107
return {
127108
...rest,
128109
workflowExecutionTimeout: workflowExecutionTimeout ? msToTs(workflowExecutionTimeout) : undefined,

packages/test/src/load/starter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import arg from 'arg';
22
import pidusage from 'pidusage';
3+
import { v4 as uuid4 } from 'uuid';
34
import { interval, range, Observable, OperatorFunction, ReplaySubject, pipe, lastValueFrom } from 'rxjs';
45
import { bufferTime, map, mergeMap, tap, takeUntil } from 'rxjs/operators';
56
import { Connection, WorkflowClient } from '@temporalio/client';
67
import { toMB } from '@temporalio/worker/lib/utils';
78
import { StarterArgSpec, starterArgSpec, getRequired } from './args';
89

910
async function runWorkflow(client: WorkflowClient, name: string, taskQueue: string) {
10-
await client.execute(name, { args: [], taskQueue });
11+
await client.execute(name, { args: [], taskQueue, workflowId: uuid4() });
1112
}
1213

1314
class NumberOfWorkflows {

0 commit comments

Comments
 (0)