@temporalio/client
Version:
Temporal.io SDK Client sub-package
163 lines (154 loc) • 7.13 kB
text/typescript
import { ServiceError as GrpcServiceError, status as grpcStatus } from '@grpc/grpc-js';
import { decodePriority, LoadedDataConverter, NamespaceNotFoundError } from '@temporalio/common';
import {
decodeSearchAttributes,
decodeTypedSearchAttributes,
searchAttributePayloadConverter,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { Replace } from '@temporalio/common/lib/type-helpers';
import { optionalTsToDate, requiredTsToDate } from '@temporalio/common/lib/time';
import { decodeMapFromPayloads } from '@temporalio/common/lib/internal-non-workflow/codec-helpers';
import { temporal, google } from '@temporalio/proto';
import {
CountWorkflowExecution,
RawWorkflowExecutionInfo,
WorkflowExecutionInfo,
WorkflowExecutionStatusName,
} from './types';
function workflowStatusCodeToName(code: temporal.api.enums.v1.WorkflowExecutionStatus): WorkflowExecutionStatusName {
return workflowStatusCodeToNameInternal(code) ?? 'UNKNOWN';
}
/**
* Intentionally leave out `default` branch to get compilation errors when new values are added
*/
function workflowStatusCodeToNameInternal(
code: temporal.api.enums.v1.WorkflowExecutionStatus
): WorkflowExecutionStatusName {
switch (code) {
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_UNSPECIFIED:
return 'UNSPECIFIED';
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_RUNNING:
return 'RUNNING';
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED:
return 'FAILED';
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TIMED_OUT:
return 'TIMED_OUT';
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CANCELED:
return 'CANCELLED';
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_TERMINATED:
return 'TERMINATED';
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_COMPLETED:
return 'COMPLETED';
case temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_CONTINUED_AS_NEW:
return 'CONTINUED_AS_NEW';
}
}
export async function executionInfoFromRaw<T>(
raw: RawWorkflowExecutionInfo,
dataConverter: LoadedDataConverter,
rawDataToEmbed: T
): Promise<Replace<WorkflowExecutionInfo, { raw: T }>> {
return {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
type: raw.type!.name!,
workflowId: raw.execution!.workflowId!,
runId: raw.execution!.runId!,
taskQueue: raw.taskQueue!,
status: {
code: raw.status!,
name: workflowStatusCodeToName(raw.status!),
},
// Safe to convert to number, max history length is 50k, which is much less than Number.MAX_SAFE_INTEGER
historyLength: raw.historyLength!.toNumber(),
// Exact truncation for multi-petabyte histories
// historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
historySize: raw.historySizeBytes?.toNumber() || undefined,
startTime: requiredTsToDate(raw.startTime, 'startTime'),
executionTime: optionalTsToDate(raw.executionTime),
closeTime: optionalTsToDate(raw.closeTime),
memo: await decodeMapFromPayloads(dataConverter, raw.memo?.fields),
searchAttributes: decodeSearchAttributes(raw.searchAttributes?.indexedFields),
typedSearchAttributes: decodeTypedSearchAttributes(raw.searchAttributes?.indexedFields),
parentExecution: raw.parentExecution
? {
workflowId: raw.parentExecution.workflowId!,
runId: raw.parentExecution.runId!,
}
: undefined,
rootExecution: raw.rootExecution
? {
workflowId: raw.rootExecution.workflowId!,
runId: raw.rootExecution.runId!,
}
: undefined,
raw: rawDataToEmbed,
priority: decodePriority(raw.priority),
};
}
export function decodeCountWorkflowExecutionsResponse(
raw: temporal.api.workflowservice.v1.ICountWorkflowExecutionsResponse
): CountWorkflowExecution {
return {
// Note: lossy conversion of Long to number
count: raw.count!.toNumber(),
groups: raw.groups!.map((group) => {
return {
// Note: lossy conversion of Long to number
count: group.count!.toNumber(),
groupValues: group.groupValues!.map((value) => searchAttributePayloadConverter.fromPayload(value)),
};
}),
};
}
type ErrorDetailsName = `temporal.api.errordetails.v1.${keyof typeof temporal.api.errordetails.v1}`;
type FailureName = `temporal.api.failure.v1.${keyof typeof temporal.api.failure.v1}`;
/**
* If the error type can be determined based on embedded grpc error details,
* then rethrow the appropriate TypeScript error. Otherwise do nothing.
*
* This function should be used before falling back to generic error handling
* based on grpc error code. Very few error types are currently supported, but
* this function will be expanded over time as more server error types are added.
*/
export function rethrowKnownErrorTypes(err: GrpcServiceError): void {
// We really don't expect multiple error details, but this really is an array, so just in case...
for (const entry of getGrpcStatusDetails(err) ?? []) {
if (!entry.type_url || !entry.value) continue;
const type = entry.type_url.replace(/^type.googleapis.com\//, '') as ErrorDetailsName;
switch (type) {
case 'temporal.api.errordetails.v1.NamespaceNotFoundFailure': {
const { namespace } = temporal.api.errordetails.v1.NamespaceNotFoundFailure.decode(entry.value);
throw new NamespaceNotFoundError(namespace);
}
case 'temporal.api.errordetails.v1.MultiOperationExecutionFailure': {
// MultiOperationExecutionFailure contains error statuses for multiple
// operations. A MultiOperationExecutionAborted error status means that
// the corresponding operation was aborted due to an error in one of the
// other operations. We rethrow the first operation error that is not
// MultiOperationExecutionAborted.
const { statuses } = temporal.api.errordetails.v1.MultiOperationExecutionFailure.decode(entry.value);
for (const status of statuses) {
const detail = status.details?.[0];
const statusType = detail?.type_url?.replace(/^type.googleapis.com\//, '') as FailureName | undefined;
if (
statusType === 'temporal.api.failure.v1.MultiOperationExecutionAborted' ||
status.code === grpcStatus.OK
) {
continue;
}
err.message = status.message ?? err.message;
err.code = status.code || err.code;
err.details = detail?.value?.toString() || err.details;
throw err;
}
}
}
}
}
function getGrpcStatusDetails(err: GrpcServiceError): google.rpc.Status['details'] | undefined {
const statusBuffer = err.metadata.get('grpc-status-details-bin')?.[0];
if (!statusBuffer || typeof statusBuffer === 'string') {
return undefined;
}
return google.rpc.Status.decode(statusBuffer).details;
}