Skip to content

Commit a75d59a

Browse files
committed
feat(workflow): propagate TypeInfo through workflows
1 parent 7b7bacc commit a75d59a

11 files changed

Lines changed: 764 additions & 47 deletions

File tree

packages/client/src/workflow-client.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
WorkflowResultType,
1212
WorkflowIdConflictPolicy,
1313
WorkflowSerializationContext,
14+
WorkflowTypeOptions,
15+
PayloadTypeInfo,
1416
} from '@temporalio/common';
1517
import {
1618
CancelledFailure,
@@ -21,11 +23,11 @@ import {
2123
TimeoutType,
2224
WorkflowExecutionAlreadyStartedError,
2325
WorkflowNotFoundError,
24-
extractWorkflowType,
2526
encodeWorkflowIdReusePolicy,
2627
decodeRetryState,
2728
encodeWorkflowIdConflictPolicy,
2829
compilePriority,
30+
extractWorkflowTypeAndConfig,
2931
} from '@temporalio/common';
3032
import { encodeUserMetadata } from '@temporalio/common/lib/internal-non-workflow/codec-helpers';
3133
import { encodeUnifiedSearchAttributes } from '@temporalio/common/lib/converter/payload-search-attributes';
@@ -360,6 +362,16 @@ export interface WorkflowResultOptions {
360362
* @default true
361363
*/
362364
followRuns?: boolean;
365+
366+
/**
367+
* Type information used to decode the Workflow result.
368+
*
369+
* This is only needed when getting a result from an existing Workflow handle or
370+
* when the Workflow definition is not available to this client.
371+
*
372+
* @experimental
373+
*/
374+
typeInfo?: PayloadTypeInfo;
363375
}
364376

365377
/**
@@ -550,13 +562,16 @@ export class WorkflowClient extends BaseClient {
550562
}
551563

552564
protected async _start<T extends Workflow>(
553-
workflowTypeOrFunc: string | T,
565+
workflowTypeOptions: WorkflowTypeOptions,
554566
options: WorkflowStartOptions<T>,
555567
interceptors: WorkflowClientInterceptor[]
556568
): Promise<WorkflowStartOutput> {
557-
const workflowType = extractWorkflowType(workflowTypeOrFunc);
558569
assertRequiredWorkflowOptions(options);
559-
const compiledOptions = compileWorkflowOptions(ensureArgs(options));
570+
const workflowOptions = {
571+
...options,
572+
typeInfo: workflowTypeOptions.typeInfo,
573+
};
574+
const compiledOptions = compileWorkflowOptions(ensureArgs(workflowOptions));
560575
const adaptedInterceptors = interceptors.map((i) => adaptWorkflowClientInterceptor(i));
561576

562577
const startWithDetails = composeInterceptors(
@@ -568,7 +583,7 @@ export class WorkflowClient extends BaseClient {
568583
return startWithDetails({
569584
options: compiledOptions,
570585
headers: {},
571-
workflowType,
586+
workflowType: workflowTypeOptions.type,
572587
});
573588
}
574589

@@ -577,10 +592,14 @@ export class WorkflowClient extends BaseClient {
577592
options: WithWorkflowArgs<T, WorkflowSignalWithStartOptions<SA>>,
578593
interceptors: WorkflowClientInterceptor[]
579594
): Promise<string> {
580-
const workflowType = extractWorkflowType(workflowTypeOrFunc);
581595
const { signal, signalArgs, ...rest } = options;
596+
const { type: workflowType, typeInfo } = extractWorkflowTypeAndConfig(workflowTypeOrFunc, rest.typeInfo);
582597
assertRequiredWorkflowOptions(rest);
583-
const compiledOptions = compileWorkflowOptions(ensureArgs(rest));
598+
const workflowOptions = {
599+
...rest,
600+
typeInfo,
601+
};
602+
const compiledOptions = compileWorkflowOptions(ensureArgs(workflowOptions));
584603
const signalWithStart = composeInterceptors(
585604
interceptors,
586605
'signalWithStart',
@@ -607,7 +626,8 @@ export class WorkflowClient extends BaseClient {
607626
): Promise<WorkflowHandleWithStartDetails<T>> {
608627
const { workflowId } = options;
609628
const interceptors = this.getOrMakeInterceptors(workflowId);
610-
const wfStartOutput = await this._start(workflowTypeOrFunc, { ...options, workflowId }, interceptors);
629+
const workflowTypeOptions = extractWorkflowTypeAndConfig(workflowTypeOrFunc, options.typeInfo);
630+
const wfStartOutput = await this._start(workflowTypeOptions, { ...options, workflowId }, interceptors);
611631
// runId is not used in handles created with `start*` calls because these
612632
// handles should allow interacting with the workflow if it continues as new.
613633
const baseHandle = this._createWorkflowHandle({
@@ -617,6 +637,7 @@ export class WorkflowClient extends BaseClient {
617637
runIdForResult: wfStartOutput.runId,
618638
interceptors,
619639
followRuns: options.followRuns ?? true,
640+
typeInfo: workflowTypeOptions.typeInfo,
620641
});
621642
return {
622643
...baseHandle,
@@ -642,6 +663,7 @@ export class WorkflowClient extends BaseClient {
642663
): Promise<WorkflowHandleWithSignaledRunId<WorkflowFn>> {
643664
const { workflowId } = options;
644665
const interceptors = this.getOrMakeInterceptors(workflowId);
666+
const workflowTypeOptions = extractWorkflowTypeAndConfig(workflowTypeOrFunc, options.typeInfo);
645667
const runId = await this._signalWithStart(workflowTypeOrFunc, options, interceptors);
646668
// runId is not used in handles created with `start*` calls because these
647669
// handles should allow interacting with the workflow if it continues as new.
@@ -652,6 +674,7 @@ export class WorkflowClient extends BaseClient {
652674
runIdForResult: runId,
653675
interceptors,
654676
followRuns: options.followRuns ?? true,
677+
typeInfo: workflowTypeOptions.typeInfo,
655678
}) as WorkflowHandleWithSignaledRunId<WorkflowFn>; // Cast is safe because we know we add the signaledRunId below
656679
(handle as any) /* readonly */.signaledRunId = runId;
657680
return handle;
@@ -736,11 +759,16 @@ export class WorkflowClient extends BaseClient {
736759
throw new Error('This WithStartWorkflowOperation instance has already been executed.');
737760
}
738761
startWorkflowOperation[withStartWorkflowOperationUsed] = true;
762+
const { type: workflowType, typeInfo } = extractWorkflowTypeAndConfig(workflowTypeOrFunc, workflowOptions.typeInfo);
739763
assertRequiredWorkflowOptions(workflowOptions);
740764

765+
const resolvedWorkflowOptions = {
766+
...workflowOptions,
767+
typeInfo,
768+
};
741769
const startUpdateWithStartInput: WorkflowStartUpdateWithStartInput = {
742-
workflowType: extractWorkflowType(workflowTypeOrFunc),
743-
workflowStartOptions: compileWorkflowOptions(ensureArgs(workflowOptions)),
770+
workflowType,
771+
workflowStartOptions: compileWorkflowOptions(ensureArgs(resolvedWorkflowOptions)),
744772
workflowStartHeaders: {},
745773
updateName: typeof updateDef === 'string' ? updateDef : updateDef.name,
746774
updateArgs: args ?? [],
@@ -757,6 +785,7 @@ export class WorkflowClient extends BaseClient {
757785
firstExecutionRunId: startResponse.runId ?? undefined,
758786
interceptors,
759787
followRuns: workflowOptions.followRuns ?? true,
788+
typeInfo,
760789
})
761790
);
762791

@@ -798,10 +827,12 @@ export class WorkflowClient extends BaseClient {
798827
): Promise<WorkflowResultType<T>> {
799828
const { workflowId } = options;
800829
const interceptors = this.getOrMakeInterceptors(workflowId);
801-
await this._start(workflowTypeOrFunc, options, interceptors);
830+
const workflowTypeOptions = extractWorkflowTypeAndConfig(workflowTypeOrFunc, options.typeInfo);
831+
await this._start(workflowTypeOptions, options, interceptors);
802832
return await this.result(workflowId, undefined, {
803833
...options,
804834
followRuns: options.followRuns ?? true,
835+
typeInfo: workflowTypeOptions.typeInfo,
805836
});
806837
}
807838

@@ -857,12 +888,14 @@ export class WorkflowClient extends BaseClient {
857888
}
858889
// Note that we can only return one value from our workflow function in JS.
859890
// Ignore any other payloads in result
860-
const [result] = await decodeArrayFromPayloads(
891+
const result = await decodeFromPayloadsAtIndex<WorkflowResultType<T>>(
861892
dataConverter,
893+
0,
862894
ev.workflowExecutionCompletedEventAttributes.result?.payloads,
863-
context
895+
context,
896+
opts?.typeInfo?.outputType
864897
);
865-
return result as any;
898+
return result;
866899
} else if (ev.workflowExecutionFailedEventAttributes) {
867900
if (followRuns && ev.workflowExecutionFailedEventAttributes.newExecutionRunId) {
868901
execution.runId = ev.workflowExecutionFailedEventAttributes.newExecutionRunId;
@@ -1335,7 +1368,9 @@ export class WorkflowClient extends BaseClient {
13351368
workflowIdReusePolicy: encodeWorkflowIdReusePolicy(options.workflowIdReusePolicy),
13361369
workflowIdConflictPolicy: encodeWorkflowIdConflictPolicy(options.workflowIdConflictPolicy),
13371370
workflowType: { name: workflowType },
1338-
input: { payloads: await encodeToPayloadsWithContext(dataConverter, context, options.args) },
1371+
input: {
1372+
payloads: await encodeToPayloadsWithContext(dataConverter, context, options.args, options.typeInfo?.inputTypes),
1373+
},
13391374
signalName,
13401375
signalInput: { payloads: await encodeToPayloadsWithContext(dataConverter, context, signalArgs) },
13411376
taskQueue: {
@@ -1465,7 +1500,9 @@ export class WorkflowClient extends BaseClient {
14651500
workflowIdReusePolicy: encodeWorkflowIdReusePolicy(opts.workflowIdReusePolicy),
14661501
workflowIdConflictPolicy: encodeWorkflowIdConflictPolicy(opts.workflowIdConflictPolicy),
14671502
workflowType: { name: workflowType },
1468-
input: { payloads: await encodeToPayloadsWithContext(dataConverter, context, opts.args) },
1503+
input: {
1504+
payloads: await encodeToPayloadsWithContext(dataConverter, context, opts.args, opts.typeInfo?.inputTypes),
1505+
},
14691506
taskQueue: {
14701507
kind: temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL,
14711508
name: opts.taskQueue,
@@ -1752,6 +1789,7 @@ export class WorkflowClient extends BaseClient {
17521789
runIdForResult: runId ?? options?.firstExecutionRunId,
17531790
interceptors,
17541791
followRuns: options?.followRuns ?? true,
1792+
typeInfo: options?.typeInfo,
17551793
});
17561794
}
17571795

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import test from 'ava';
2+
import type { PayloadTypeInfo } from '../type-info';
3+
import { extractWorkflowType, extractWorkflowTypeAndConfig } from '../workflow-options';
4+
5+
const typeInfo: PayloadTypeInfo = {
6+
inputTypes: [{ hint: { converter: 'json' } }],
7+
outputType: { hint: { converter: 'json' } },
8+
};
9+
10+
test('resolves call-site type information for a string Workflow type', (t) => {
11+
t.deepEqual(extractWorkflowTypeAndConfig('workflow', typeInfo), {
12+
type: 'workflow',
13+
typeInfo,
14+
});
15+
});
16+
17+
test('resolves definition-supplied type information for a Workflow function', (t) => {
18+
const workflow = Object.assign(async function workflow(): Promise<void> {}, {
19+
staticOptions: { typeInfo },
20+
});
21+
22+
t.deepEqual(extractWorkflowTypeAndConfig(workflow), {
23+
type: 'workflow',
24+
typeInfo,
25+
});
26+
t.is(extractWorkflowType(workflow), 'workflow');
27+
});
28+
29+
test('rejects call-site type information for a Workflow function', (t) => {
30+
async function workflow(): Promise<void> {}
31+
32+
t.throws(() => extractWorkflowTypeAndConfig(workflow, typeInfo), {
33+
instanceOf: TypeError,
34+
message: /Workflow type information cannot be supplied at the call site/,
35+
});
36+
});
37+
38+
test('ignores explicitly undefined static options', (t) => {
39+
const workflow = Object.assign(async function workflow(): Promise<void> {}, {
40+
staticOptions: undefined,
41+
});
42+
43+
t.deepEqual(extractWorkflowTypeAndConfig(workflow), {
44+
type: 'workflow',
45+
typeInfo: undefined,
46+
});
47+
});

packages/common/src/workflow-definition-options.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
import type { PayloadTypeInfo } from './type-info';
12
import type { VersioningBehavior } from './worker-deployments';
23

4+
/**
5+
* Options that can be attached to a Workflow function at definition time.
6+
*
7+
* @experimental
8+
*/
9+
export interface WorkflowDefinitionConfig {
10+
/** Options that may be evaluated for each Workflow Execution. */
11+
workflowDefinitionOptions?: WorkflowDefinitionOptionsOrGetter;
12+
/** Static metadata that must be available before Workflow input decoding. */
13+
staticOptions?: WorkflowStaticOptions;
14+
}
15+
316
/**
417
* Options that can be used when defining a workflow via {@link setWorkflowOptions}.
518
*/
@@ -30,6 +43,15 @@ export interface WorkflowDefinitionOptions {
3043
failureExceptionTypes?: Array<new (...args: any[]) => Error>;
3144
}
3245

46+
export interface WorkflowStaticOptions {
47+
/**
48+
* Type information used to encode and decode Workflow input and output.
49+
*
50+
* @experimental
51+
*/
52+
typeInfo?: PayloadTypeInfo;
53+
}
54+
3355
type AsyncFunction<Args extends any[], ReturnType> = (...args: Args) => Promise<ReturnType>;
3456
export type WorkflowDefinitionOptionsOrGetter = WorkflowDefinitionOptions | (() => WorkflowDefinitionOptions);
3557

@@ -41,3 +63,31 @@ export type WorkflowDefinitionOptionsOrGetter = WorkflowDefinitionOptions | (()
4163
export interface WorkflowFunctionWithOptions<Args extends any[], ReturnType> extends AsyncFunction<Args, ReturnType> {
4264
workflowDefinitionOptions: WorkflowDefinitionOptionsOrGetter;
4365
}
66+
67+
/** @internal */
68+
export interface WorkflowFunctionWithStaticOptions<Args extends any[], ReturnType>
69+
extends AsyncFunction<Args, ReturnType> {
70+
staticOptions: WorkflowStaticOptions;
71+
}
72+
73+
const workflowDefinitionOptionsProperty = 'workflowDefinitionOptions' satisfies keyof WorkflowFunctionWithOptions<
74+
any[],
75+
any
76+
>;
77+
const workflowStaticOptionsProperty = 'staticOptions' satisfies keyof WorkflowFunctionWithStaticOptions<any[], any>;
78+
79+
/** @internal */
80+
export function isWorkflowFunctionWithOptions(obj: unknown): obj is WorkflowFunctionWithOptions<any[], any> {
81+
return typeof obj === 'function' && Object.hasOwn(obj, workflowDefinitionOptionsProperty);
82+
}
83+
84+
/** @internal */
85+
export function isWorkflowFunctionWithStaticOptions(
86+
obj: unknown
87+
): obj is WorkflowFunctionWithStaticOptions<any[], any> {
88+
if (typeof obj !== 'function' || !Object.hasOwn(obj, workflowStaticOptionsProperty)) {
89+
return false;
90+
}
91+
const { staticOptions } = obj as { staticOptions?: unknown };
92+
return typeof staticOptions === 'object' && staticOptions !== null;
93+
}

packages/common/src/workflow-options.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type { Duration } from './time';
55
import { makeProtoEnumConverters } from './internal-workflow';
66
import type { SearchAttributePair, SearchAttributes, TypedSearchAttributes } from './search-attributes';
77
import type { Priority } from './priority';
8-
import type { WorkflowFunctionWithOptions } from './workflow-definition-options';
9-
8+
import { isWorkflowFunctionWithStaticOptions } from './workflow-definition-options';
9+
import type { PayloadTypeInfo } from './type-info';
1010
/**
1111
* Defines what happens when trying to start a Workflow with the same ID as a *Closed* Workflow.
1212
*
@@ -208,6 +208,13 @@ export interface BaseWorkflowOptions {
208208
* Priority of a workflow
209209
*/
210210
priority?: Priority;
211+
212+
/**
213+
* Type information used to encode and decode Workflow input and output.
214+
*
215+
* @experimental
216+
*/
217+
typeInfo?: PayloadTypeInfo;
211218
}
212219

213220
export type WithWorkflowArgs<W extends Workflow, T> = T &
@@ -255,15 +262,42 @@ export interface WorkflowDurationOptions {
255262

256263
export type CommonWorkflowOptions = BaseWorkflowOptions & WorkflowDurationOptions;
257264

258-
export function extractWorkflowType<T extends Workflow>(
259-
workflowTypeOrFunc: string | T | WorkflowFunctionWithOptions<any[], any>
260-
): string {
261-
if (typeof workflowTypeOrFunc === 'string') return workflowTypeOrFunc as string;
265+
export interface WorkflowTypeOptions {
266+
type: string;
267+
typeInfo?: PayloadTypeInfo;
268+
}
269+
270+
export function extractWorkflowTypeAndConfig<T extends Workflow>(
271+
workflowTypeOrFunc: string | T,
272+
callSiteTypeInfo?: PayloadTypeInfo
273+
): WorkflowTypeOptions {
274+
if (typeof workflowTypeOrFunc === 'string') {
275+
return { type: workflowTypeOrFunc, typeInfo: callSiteTypeInfo };
276+
}
262277
if (typeof workflowTypeOrFunc === 'function') {
263-
if (workflowTypeOrFunc?.name) return workflowTypeOrFunc.name;
264-
throw new TypeError('Invalid workflow type: the workflow function is anonymous');
278+
if (!workflowTypeOrFunc.name) {
279+
throw new TypeError('Invalid workflow type: the workflow function is anonymous');
280+
}
281+
if (callSiteTypeInfo !== undefined) {
282+
throw new TypeError(
283+
'Workflow type information cannot be supplied at the call site when using a workflow function. ' +
284+
'Use defineWorkflowOptions(..., { staticOptions: { typeInfo } }) on the workflow function instead, ' +
285+
'or pass the workflow type as a string.'
286+
);
287+
}
288+
const definitionTypeInfo = isWorkflowFunctionWithStaticOptions(workflowTypeOrFunc)
289+
? workflowTypeOrFunc.staticOptions.typeInfo
290+
: undefined;
291+
return {
292+
type: workflowTypeOrFunc.name,
293+
typeInfo: definitionTypeInfo,
294+
};
265295
}
266296
throw new TypeError(
267297
`Invalid workflow type: expected either a string or a function, got '${typeof workflowTypeOrFunc}'`
268298
);
269299
}
300+
301+
export function extractWorkflowType<T extends Workflow>(workflowTypeOrFunc: string | T): string {
302+
return extractWorkflowTypeAndConfig(workflowTypeOrFunc).type;
303+
}

0 commit comments

Comments
 (0)