@temporalio/client
Version:
Temporal.io SDK Client sub-package
419 lines • 19.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActivityClient = void 0;
const grpc_js_1 = require("@grpc/grpc-js");
const uuid_1 = require("uuid");
const common_1 = require("@temporalio/common");
const time_1 = require("@temporalio/common/lib/time");
const interceptors_1 = require("@temporalio/common/lib/interceptors");
const payload_search_attributes_1 = require("@temporalio/common/lib/converter/payload-search-attributes");
const internal_non_workflow_1 = require("@temporalio/common/lib/internal-non-workflow");
const proto_1 = require("@temporalio/proto");
const async_completion_client_1 = require("./async-completion-client");
const types_1 = require("./types");
const helpers_1 = require("./helpers");
const errors_1 = require("./errors");
/**
* Client for starting and managing Activities, and for asynchronous completion and heartbeating of Activities.
* Includes all functionality of {@link AsyncCompletionClient}.
*
* Typically this client should not be instantiated directly, instead create the high level {@link Client} and use
* {@link Client.activity} to interact with Activities.
*/
class ActivityClient extends async_completion_client_1.AsyncCompletionClient {
interceptedHandlers;
constructor(options) {
super(options);
const interceptors = options?.interceptors ?? [];
this.interceptedHandlers = {
start: (0, interceptors_1.composeInterceptors)(interceptors, 'start', this.startHandler.bind(this)),
getResult: (0, interceptors_1.composeInterceptors)(interceptors, 'getResult', this.getResultHandler.bind(this)),
describe: (0, interceptors_1.composeInterceptors)(interceptors, 'describe', this.describeHandler.bind(this)),
cancel: (0, interceptors_1.composeInterceptors)(interceptors, 'cancel', this.cancelHandler.bind(this)),
terminate: (0, interceptors_1.composeInterceptors)(interceptors, 'terminate', this.terminateHandler.bind(this)),
list: (0, interceptors_1.composeInterceptors)(interceptors, 'list', this.listHandler.bind(this)),
count: (0, interceptors_1.composeInterceptors)(interceptors, 'count', this.countHandler.bind(this)),
};
}
/**
* Returns this client as a {@link TypedActivityClient}. It enables strong type checking of Activity name, arguments
* and result based on the provided Activity interface. Note that no new client object is created - this method only
* affects type annotations.
* @template T Activity interface to use for type checking. The returned client can only start activities present in
* this interface.
*
* @experimental Standalone Activities are experimental. APIs may be subject to change.
*/
typed() {
return this;
}
/**
* Starts new Standalone Activity execution.
*
* @param activity Name of the activity to start.
* @param options Options controlling the start and execution of the activity.
* @returns Handle to the started activity. The handle's `runId` property will be set to the started run.
*
* @experimental Standalone Activities are experimental. APIs may be subject to change.
*/
async start(activity, options) {
return this.interceptedHandlers.start({
activityType: activity,
options,
headers: {},
});
}
/**
* Executes a Standalone Activity until completion and returns the result.
* @param activity Name of the activity to start.
* @param options Options controlling the activity execution.
* @returns Result of the activity.
*
* @experimental Standalone Activities are experimental. APIs may be subject to change.
*/
async execute(activity, options) {
const handle = await this.start(activity, options);
return handle.result();
}
/**
* Creates an Activity handle from ID and optionally from run ID. If `runId` is not set, the handle will refer to the
* newest Activity run with the given Activity ID.
*
* Note 1: this function always succeeds. If the provided ID is invalid, an error will only be thrown when calling
* the handle's methods.
*
* Note 2: if `runID` is not set when calling `getHandle`, then `runId` property of the returned handle will always
* remain unset, even after method calls are performed. To get the run ID of the targeted activity execution, call
* {@link ActivityHandle.describe} and read the `activityRunId` field of the returned {@link ActivityExecutionDescription}.
*
* @param activityId ID of the Activity.
* @param runId Optional run ID of the specific Activity execution.
* @returns Handle to the specified activity execution.
*
* @experimental Standalone Activities are experimental. APIs may be subject to change.
*/
getHandle(activityId, runId) {
return this.createHandle(activityId, runId);
}
/**
* Return a list of Activity executions matching the given `query`.
*
* Note that the list of Activity executions returned is approximate and eventually consistent.
*
* More info on the concept of "visibility" and the query syntax on the Temporal documentation site:
* https://docs.temporal.io/visibility
*
* @experimental Standalone Activities are experimental. APIs may be subject to change.
*/
list(query) {
return this.interceptedHandlers.list({
query,
headers: {},
});
}
/**
* Return the number of Activity executions matching the given `query`.
*
* Note that the number of Activity executions returned is approximate and eventually consistent.
*
* More info on the concept of "visibility" and the query syntax on the Temporal documentation site:
* https://docs.temporal.io/visibility
*
* @experimental Standalone Activities are experimental. APIs may be subject to change.
*/
async count(query) {
return await this.interceptedHandlers.count({
query,
headers: {},
});
}
createHandle(activityId, runId) {
if (!activityId) {
throw new TypeError('activityId is required');
}
const handle = {
client: this,
activityId,
runId,
async result() {
return await this.client.interceptedHandlers.getResult({
activityId: this.activityId,
activityRunId: this.runId ?? '',
headers: {},
});
},
async describe() {
return await this.client.interceptedHandlers.describe({
activityId: this.activityId,
activityRunId: this.runId ?? '',
headers: {},
});
},
async cancel(reason) {
return await this.client.interceptedHandlers.cancel({
activityId: this.activityId,
activityRunId: this.runId ?? '',
reason,
headers: {},
});
},
async terminate(reason) {
return await this.client.interceptedHandlers.terminate({
activityId: this.activityId,
activityRunId: this.runId ?? '',
reason,
headers: {},
});
},
};
return handle;
}
async startHandler(input) {
if (!input.activityType) {
throw new TypeError('activityType is required');
}
validateActivityOptions(input.options);
try {
const resp = await this.workflowService.startActivityExecution(await this.buildStartActivityExecutionRequest(input));
return this.createHandle(input.options.id, resp.runId);
}
catch (err) {
if ((0, errors_1.isGrpcServiceError)(err) && err.code === grpc_js_1.status.ALREADY_EXISTS) {
for (const entry of (0, helpers_1.getGrpcStatusDetails)(err) ?? []) {
if (!entry.type_url || !entry.value)
continue;
if ((0, helpers_1.trimGrpcTypeUrl)(entry.type_url) ===
'temporal.api.errordetails.v1.ActivityExecutionAlreadyStartedFailure') {
const details = proto_1.temporal.api.errordetails.v1.ActivityExecutionAlreadyStartedFailure.decode(entry.value);
throw new errors_1.ActivityExecutionAlreadyStartedError('Activity execution already started', input.options.id, details.runId);
}
}
}
this.rethrowGrpcError(err, 'Failed to start activity');
}
}
async buildStartActivityExecutionRequest(input) {
const searchAttributes = input.options.typedSearchAttributes
? { indexedFields: (0, payload_search_attributes_1.encodeUnifiedSearchAttributes)(undefined, input.options.typedSearchAttributes) }
: undefined;
return {
namespace: this.options.namespace,
identity: this.options.identity,
requestId: (0, uuid_1.v4)(),
activityId: input.options.id,
activityType: { name: input.activityType },
taskQueue: { name: input.options.taskQueue },
scheduleToCloseTimeout: (0, time_1.msOptionalToTs)(input.options.scheduleToCloseTimeout),
scheduleToStartTimeout: (0, time_1.msOptionalToTs)(input.options.scheduleToStartTimeout),
startToCloseTimeout: (0, time_1.msOptionalToTs)(input.options.startToCloseTimeout),
heartbeatTimeout: (0, time_1.msOptionalToTs)(input.options.heartbeatTimeout),
retryPolicy: input.options.retry ? (0, common_1.compileRetryPolicy)(input.options.retry) : undefined,
input: { payloads: await (0, internal_non_workflow_1.encodeToPayloads)(this.dataConverter, ...(input.options.args || [])) },
idReusePolicy: (0, types_1.encodeActivityIdReusePolicy)(input.options.idReusePolicy),
idConflictPolicy: (0, types_1.encodeActivityIdConflictPolicy)(input.options.idConflictPolicy),
searchAttributes,
header: { fields: input.headers },
userMetadata: await (0, internal_non_workflow_1.encodeUserMetadata)(this.dataConverter, input.options.summary, undefined),
priority: input.options.priority ? (0, common_1.compilePriority)(input.options.priority) : undefined,
};
}
async getResultHandler(input) {
if (!input.activityId) {
throw new TypeError('activityId is required');
}
const req = {
namespace: this.options.namespace,
activityId: input.activityId,
runId: input.activityRunId || undefined,
};
for (;;) {
let failedErr;
try {
const resp = await this.workflowService.pollActivityExecution(req);
if (resp.outcome?.result) {
const [result] = await (0, internal_non_workflow_1.decodeArrayFromPayloads)(this.dataConverter, resp.outcome.result.payloads ?? []);
return result;
}
else if (resp.outcome?.failure) {
// If error conversion throws an exception, we want it to be caught and handled by rethrowGrpcError().
// If it succeeds, we want to throw the ActivityExecutionFailedError directly, so outside of try/catch.
failedErr = new errors_1.ActivityExecutionFailedError('Activity execution failed', await (0, internal_non_workflow_1.decodeOptionalFailureToOptionalError)(this.dataConverter, resp.outcome.failure), input.activityId, resp.runId || input.activityRunId || undefined);
}
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to get activity result');
}
if (failedErr) {
throw failedErr;
}
}
}
async describeHandler(input) {
if (!input.activityId) {
throw new TypeError('activityId is required');
}
try {
const resp = await this.workflowService.describeActivityExecution({
namespace: this.options.namespace,
activityId: input.activityId,
runId: input.activityRunId || undefined,
});
return buildActivityDescription(resp.info, this.dataConverter);
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to describe activity');
}
}
async cancelHandler(input) {
if (!input.activityId) {
throw new TypeError('activityId is required');
}
try {
await this.workflowService.requestCancelActivityExecution({
namespace: this.options.namespace,
activityId: input.activityId,
runId: input.activityRunId || undefined,
identity: this.options.identity,
requestId: (0, uuid_1.v4)(),
reason: input.reason || undefined,
});
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to request activity cancellation');
}
}
async terminateHandler(input) {
if (!input.activityId) {
throw new TypeError('activityId is required');
}
try {
await this.workflowService.terminateActivityExecution({
namespace: this.options.namespace,
activityId: input.activityId,
runId: input.activityRunId || undefined,
identity: this.options.identity,
requestId: (0, uuid_1.v4)(),
reason: input.reason || undefined,
});
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to terminate activity');
}
}
async *listHandler(input) {
let nextPageToken = undefined;
do {
try {
const resp = await this.workflowService.listActivityExecutions({
namespace: this.options.namespace,
query: input.query,
nextPageToken,
});
for (const info of resp.executions ?? []) {
yield buildActivityExecutionInfo(info);
}
nextPageToken = resp.nextPageToken;
}
catch (e) {
this.rethrowGrpcError(e, 'Failed to list activities');
}
} while (nextPageToken && nextPageToken.length > 0);
}
async countHandler(input) {
try {
const resp = await this.workflowService.countActivityExecutions({
namespace: this.options.namespace,
query: input.query,
});
return {
count: resp.count?.toNumber() ?? 0,
groups: resp.groups?.map((g) => ({
count: g.count?.toNumber() ?? 0,
groupValues: g.groupValues?.map((v) => payload_search_attributes_1.searchAttributePayloadConverter.fromPayload(v)),
})),
};
}
catch (err) {
this.rethrowGrpcError(err, 'Failed to count activities');
}
}
rethrowGrpcError(err, fallbackMessage) {
if ((0, errors_1.isGrpcServiceError)(err)) {
(0, helpers_1.rethrowKnownErrorTypes)(err);
if (err.code === grpc_js_1.status.NOT_FOUND) {
throw new errors_1.ActivityNotFoundError(err.details ?? 'Activity not found');
}
throw new errors_1.ServiceError(fallbackMessage, { cause: err });
}
throw new errors_1.ServiceError('Unexpected error while making gRPC request');
}
}
exports.ActivityClient = ActivityClient;
function validateActivityOptions(options) {
if (!options.id) {
throw new TypeError('id is required');
}
if (!options.taskQueue) {
throw new TypeError('taskQueue is required');
}
if (!options.scheduleToCloseTimeout && !options.startToCloseTimeout) {
throw new TypeError('Either scheduleToCloseTimeout or startToCloseTimeout is required');
}
}
function buildActivityExecutionInfoCommonPart(info) {
return {
activityId: info.activityId,
activityRunId: info.runId,
activityType: info.activityType.name,
scheduleTime: (0, time_1.optionalTsToDate)(info.scheduleTime),
closeTime: (0, time_1.optionalTsToDate)(info.closeTime),
status: (0, types_1.decodeActivityExecutionStatus)(info.status),
typedSearchAttributes: (0, payload_search_attributes_1.decodeTypedSearchAttributes)(info.searchAttributes?.indexedFields),
taskQueue: info.taskQueue,
executionDurationMs: (0, time_1.optionalTsToMs)(info.executionDuration),
};
}
function buildActivityExecutionInfo(info) {
return {
...buildActivityExecutionInfoCommonPart(info),
rawListInfo: info,
};
}
function buildActivityDescription(info, dataConverter) {
const getHeartbeatDetails = async () => {
const payloads = info.heartbeatDetails?.payloads;
if (payloads && payloads.length > 0) {
return await (0, internal_non_workflow_1.decodeFromPayloadsAtIndex)(dataConverter, 0, info.heartbeatDetails?.payloads);
}
else {
return undefined;
}
};
const getLastFailure = async () => {
return await (0, internal_non_workflow_1.decodeOptionalFailureToOptionalError)(dataConverter, info.lastFailure);
};
return {
...buildActivityExecutionInfoCommonPart(info),
rawInfo: info,
runState: (0, types_1.decodePendingActivityState)(info.runState),
scheduleToCloseTimeoutMs: (0, time_1.optionalTsToMs)(info.scheduleToCloseTimeout),
scheduleToStartTimeoutMs: (0, time_1.optionalTsToMs)(info.scheduleToStartTimeout),
startToCloseTimeoutMs: (0, time_1.optionalTsToMs)(info.startToCloseTimeout),
heartbeatTimeoutMs: (0, time_1.optionalTsToMs)(info.heartbeatTimeout),
retryPolicy: (0, common_1.decompileRetryPolicy)(info.retryPolicy),
lastHeartbeatTime: (0, time_1.optionalTsToDate)(info.lastHeartbeatTime),
lastStartedTime: (0, time_1.optionalTsToDate)(info.lastStartedTime),
attempt: info.attempt,
expirationTime: (0, time_1.optionalTsToDate)(info.expirationTime),
lastWorkerIdentity: info.lastWorkerIdentity || undefined,
currentRetryIntervalMs: (0, time_1.optionalTsToMs)(info.currentRetryInterval),
lastAttemptCompleteTime: (0, time_1.optionalTsToDate)(info.lastAttemptCompleteTime),
nextAttemptScheduleTime: (0, time_1.optionalTsToDate)(info.nextAttemptScheduleTime),
lastDeploymentVersion: (0, common_1.convertDeploymentVersion)(info.lastDeploymentVersion),
priority: (0, common_1.decodePriority)(info.priority),
canceledReason: info.canceledReason || undefined,
getHeartbeatDetails,
getLastFailure,
};
}
//# sourceMappingURL=activity-client.js.map