Skip to content

Commit 8a7d1f2

Browse files
committed
Added TypedActivityClient
1 parent 5c3700b commit 8a7d1f2

5 files changed

Lines changed: 343 additions & 15 deletions

File tree

packages/client/src/activity-client.ts

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { status } from '@grpc/grpc-js';
22
import { v4 as uuid4 } from 'uuid';
33
import type {
4+
ActivityFunction,
45
LoadedDataConverter,
56
Next,
67
Priority,
@@ -31,6 +32,7 @@ import {
3132
encodeUserMetadata,
3233
} from '@temporalio/common/lib/internal-non-workflow';
3334
import type { temporal } from '@temporalio/proto';
35+
import type { Replace } from '@temporalio/common/lib/type-helpers';
3436
import type {
3537
ActivityCancelInput,
3638
ActivityClientInterceptor,
@@ -73,7 +75,7 @@ export interface ActivityClientOptions extends AsyncCompletionClientOptions {
7375
* Typically this client should not be instantiated directly, instead create the high level {@link Client} and use
7476
* {@link Client.activity} to interact with Activities.
7577
*/
76-
export class ActivityClient extends AsyncCompletionClient {
78+
export class ActivityClient extends AsyncCompletionClient implements TypedActivityClient<any> {
7779
private readonly interceptedHandlers: {
7880
[K in keyof Required<ActivityClientInterceptor>]: Next<ActivityClientInterceptor, K>;
7981
};
@@ -93,14 +95,25 @@ export class ActivityClient extends AsyncCompletionClient {
9395
};
9496
}
9597

98+
/**
99+
* Returns this client as a {@link TypedActivityClient}. It enables strong type checking of Activity name, arguments
100+
* and result based on the provided Activity interface. Note that no new client object is created - this method only
101+
* affects type annotations.
102+
* @template T Activity interface to use for type checking. The returned client can only start activities present in
103+
* this interface.
104+
*/
105+
typedClient<T>(): TypedActivityClient<T> {
106+
return this;
107+
}
108+
96109
/**
97110
* Starts new Standalone Activity execution.
98111
*
99112
* @param activity Name of the activity to start.
100113
* @param options Options controlling the start and execution of the activity.
101114
* @returns Handle to the started activity. The handle's `runId` property will be set to the started run.
102115
*/
103-
async start<O = any>(activity: string, options: ActivityOptions): Promise<ActivityHandle<O>> {
116+
async start<R = any>(activity: string, options: ActivityOptions): Promise<ActivityHandle<R>> {
104117
return this.interceptedHandlers.start({
105118
activityType: activity,
106119
options,
@@ -114,7 +127,7 @@ export class ActivityClient extends AsyncCompletionClient {
114127
* @param options Options controlling the activity execution.
115128
* @returns Result of the activity.
116129
*/
117-
async execute<O = any>(activity: string, options: ActivityOptions): Promise<O> {
130+
async execute<R = any>(activity: string, options: ActivityOptions): Promise<R> {
118131
const handle = await this.start(activity, options);
119132
return handle.result();
120133
}
@@ -134,7 +147,7 @@ export class ActivityClient extends AsyncCompletionClient {
134147
* @param runId Optional run ID of the specific Activity execution.
135148
* @returns Handle to the specified activity execution.
136149
*/
137-
getHandle<O = any>(activityId: string, runId?: string): ActivityHandle<O> {
150+
getHandle<R = any>(activityId: string, runId?: string): ActivityHandle<R> {
138151
return this.createHandle(activityId, runId);
139152
}
140153

@@ -168,7 +181,7 @@ export class ActivityClient extends AsyncCompletionClient {
168181
});
169182
}
170183

171-
protected createHandle<O>(activityId: string, runId?: string): ActivityHandle<O> {
184+
protected createHandle<R>(activityId: string, runId?: string): ActivityHandle<R> {
172185
if (!activityId) {
173186
throw new TypeError('activityId is required');
174187
}
@@ -178,7 +191,7 @@ export class ActivityClient extends AsyncCompletionClient {
178191
activityId,
179192
runId,
180193

181-
async result(): Promise<O> {
194+
async result(): Promise<R> {
182195
return await this.client.interceptedHandlers.getResult({
183196
activityId: this.activityId,
184197
activityRunId: this.runId ?? '',
@@ -409,8 +422,9 @@ export class ActivityClient extends AsyncCompletionClient {
409422
/**
410423
* Handle that can be used to perform operations on the associated Activity.
411424
* Can be obtained by calling {@link ActivityClient.start} or {@link ActivityClient.getHandle}.
425+
* @template R Result type of the activity. Use {@link ActivityClient.typedClient} to start activities in a type-safe way.
412426
*/
413-
export interface ActivityHandle<O = any> {
427+
export interface ActivityHandle<R = any> {
414428
/**
415429
* ID of the Activity this handle refers to.
416430
*/
@@ -425,7 +439,7 @@ export interface ActivityHandle<O = any> {
425439
* If the activity was not successful, throws {@link ActivityExecutionFailedError}. The activity failure is stored in
426440
* the `cause` field.
427441
*/
428-
result(): Promise<O>;
442+
result(): Promise<R>;
429443
/**
430444
* Returns information about the Activity execution.
431445
*/
@@ -455,7 +469,7 @@ export interface ActivityOptions {
455469
/**
456470
* Input arguments to pass to the activity.
457471
*/
458-
args?: any[];
472+
args?: any[] | Readonly<any[]>;
459473
/**
460474
* If set, specifies maximum time between successful heartbeats.
461475
*/
@@ -579,3 +593,76 @@ function buildActivityDescription(
579593
getLastFailure,
580594
};
581595
}
596+
597+
/**
598+
* Sub-interface of {@link ActivityClient} that provides a strongly-typed interface for executing Activities.
599+
* Argument types in the provided options must match the argument types of the specified Activity as defined in provided
600+
* interface
601+
* @template T Activity interface
602+
*/
603+
export interface TypedActivityClient<T> {
604+
start<N extends ActivityName<T>>(
605+
activity: N,
606+
options: ActivityOptionsFor<T, N>
607+
): Promise<ActivityHandle<ActivityResult<T, N>>>;
608+
609+
execute<N extends ActivityName<T>>(activity: N, options: ActivityOptionsFor<T, N>): Promise<ActivityResult<T, N>>;
610+
}
611+
612+
/**
613+
* Utility type to support strong typing in {@link TypedActivityClient}.
614+
* Contains names of activities extracted from the specified activity interface.
615+
* @template T Activity interface
616+
*/
617+
export type ActivityName<T> = {
618+
[N in keyof T & string]: T[N] extends ActivityFunction<any, any> ? N : never;
619+
}[keyof T & string];
620+
621+
/**
622+
* Utility type to support strong typing in {@link TypedActivityClient}.
623+
* Extracts argument types of an activity.
624+
* @template T Activity interface
625+
* @template N Activity name
626+
*/
627+
export type ActivityArgs<T, N extends ActivityName<T>> = T[N] extends ActivityFunction<infer P, any> ? P : never;
628+
629+
/**
630+
* Utility type to support strong typing in {@link TypedActivityClient}.
631+
* Extracts result type of an activity.
632+
* @template T Activity interface
633+
* @template N Activity name
634+
*/
635+
export type ActivityResult<T, N extends ActivityName<T>> = T[N] extends ActivityFunction<any, infer R> ? R : never;
636+
637+
/**
638+
* Utility type to support strong typing in {@link TypedActivityClient}.
639+
* Represents {@link ActivityOptions} with strongly typed arguments.
640+
* @template Args Types of activity arguments as an array type.
641+
*/
642+
export type ActivityOptionsWithArgs<Args extends any[]> = Args extends [any, ...any]
643+
? Replace<
644+
ActivityOptions,
645+
{
646+
/**
647+
* Arguments to pass to the Activity
648+
*/
649+
args: Args | Readonly<Args>;
650+
}
651+
>
652+
: Replace<
653+
ActivityOptions,
654+
{
655+
/**
656+
* Arguments to pass to the Activity
657+
*/
658+
args?: Args | Readonly<Args>;
659+
}
660+
>;
661+
662+
/**
663+
* Utility type to support strong typing in {@link TypedActivityClient}.
664+
* Represents {@link ActivityOptions} with strongly typed arguments matching specified Activity in specified interface.
665+
* @template T Activity interface
666+
* @template N Activity name
667+
*/
668+
export type ActivityOptionsFor<T, N extends ActivityName<T>> = ActivityOptionsWithArgs<ActivityArgs<T, N>>;

packages/client/src/async-completion-client.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow
55
import type { BaseClientOptions, LoadedWithDefaults, WithDefaults } from './base-client';
66
import { BaseClient, defaultBaseClientOptions } from './base-client';
77
import {
8-
isGrpcServiceError, ActivityNotFoundError, ActivityCompletionError, ActivityCancelledError,
9-
ActivityResetError, ActivityPausedError,
8+
isGrpcServiceError,
9+
ActivityNotFoundError,
10+
ActivityCompletionError,
11+
ActivityCancelledError,
12+
ActivityResetError,
13+
ActivityPausedError,
1014
} from './errors';
1115
import type { WorkflowService } from './types';
1216
import { rethrowKnownErrorTypes } from './helpers';

packages/client/src/errors.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import type { ServiceError as GrpcServiceError } from '@grpc/grpc-js';
22
import { status } from '@grpc/grpc-js';
33
import type { RetryState } from '@temporalio/common';
44
import { isError, isRecord, SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers';
5-
import { AsyncCompletionClient, AsyncCompletionClientOptions } from './async-completion-client';
6-
import type { ActivityClientInterceptor } from './interceptors';
75

86
/**
97
* Generic Error class for errors coming from the service

packages/client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export * from '@temporalio/common/lib/errors';
5454
export * from '@temporalio/common/lib/interfaces';
5555
export * from '@temporalio/common/lib/workflow-handle';
5656
export * from './async-completion-client';
57+
export * from './activity-client';
5758
export * from './client';
5859
export {
5960
Connection,

0 commit comments

Comments
 (0)