UNPKG

@temporalio/common

Version:

Common library for code that's used across the Client, Worker, and/or Workflow

396 lines 19.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DefaultFailureConverter = void 0; exports.cutoffStackTrace = cutoffStackTrace; const nexus = __importStar(require("nexus-rpc")); const long_1 = __importDefault(require("long")); const failure_1 = require("../failure"); const internal_workflow_1 = require("../internal-workflow"); const type_helpers_1 = require("../type-helpers"); const time_1 = require("../time"); const encoding_1 = require("../encoding"); const payload_converter_1 = require("./payload-converter"); // Can't import proto enums into the workflow sandbox, use this helper type and enum converter instead. const NexusHandlerErrorRetryBehavior = { RETRYABLE: 'RETRYABLE', NON_RETRYABLE: 'NON_RETRYABLE', }; const [encodeNexusHandlerErrorRetryBehavior, decodeNexusHandlerErrorRetryBehavior] = (0, internal_workflow_1.makeProtoEnumConverters)({ UNSPECIFIED: 0, [NexusHandlerErrorRetryBehavior.RETRYABLE]: 1, [NexusHandlerErrorRetryBehavior.NON_RETRYABLE]: 2, }, 'NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_'); function combineRegExp(...regexps) { return new RegExp(regexps.map((x) => `(?:${x.source})`).join('|')); } /** * Stack traces will be cutoff when on of these patterns is matched */ const CUTOFF_STACK_PATTERNS = combineRegExp( /** Activity execution */ /\s+at (Activity\.)?execute \(.*[\\/]worker[\\/](?:src|lib)[\\/]activity\.[jt]s:\d+:\d+\)/, /** Nexus execution */ /\s+at( async)? (NexusHandler\.)?invokeUserCode \(.*[\\/]worker[\\/](?:src|lib)[\\/]nexus[\\/]index\.[jt]s:\d+:\d+\)/, /** Workflow activation (inbound handlers only) */ /\s+at( async)? (Activator\.)?(startWorkflow|queryWorkflow|signalWorkflow|update|validateUpdate)NextHandler \(.*\.[jt]s:\d+:\d+\)/, /** Workflow run anything in context */ /\s+at (Script\.)?runInContext \(native|unknown|(?:(?:node:vm|vm\.js):\d+:\d+)\)/); /** * Any stack trace frames that match any of those wil be dopped. * The "null." prefix on some cases is to avoid https://github.com/nodejs/node/issues/42417 */ const DROPPED_STACK_FRAMES_PATTERNS = combineRegExp( /** Internal functions used to recursively chain interceptors */ /\s+at (null\.)?next \(.*[\\/]common[\\/](?:src|lib)[\\/]interceptors\.[jt]s:\d+:\d+\)/, /** Internal functions used to recursively chain interceptors */ /\s+at (null\.)?executeNextHandler \(.*[\\/]worker[\\/](?:src|lib)[\\/]activity\.[jt]s:\d+:\d+\)/); /** * Cuts out the framework part of a stack trace, leaving only user code entries */ function cutoffStackTrace(stack) { const lines = (stack ?? '').split(/\r?\n/); const acc = Array(); for (const line of lines) { if (CUTOFF_STACK_PATTERNS.test(line)) break; if (!DROPPED_STACK_FRAMES_PATTERNS.test(line)) acc.push(line); } return acc.join('\n'); } /** * Default, cross-language-compatible Failure converter. * * By default, it will leave error messages and stack traces as plain text. In order to encrypt them, set * `encodeCommonAttributes` to `true` in the constructor options and use a {@link PayloadCodec} that can encrypt / * decrypt Payloads in your {@link WorkerOptions.dataConverter | Worker} and * {@link ClientOptions.dataConverter | Client options}. */ class DefaultFailureConverter { options; constructor(options) { const { encodeCommonAttributes } = options ?? {}; this.options = { encodeCommonAttributes: encodeCommonAttributes ?? false, }; } /** * Converts a Failure proto message to a JS Error object. * * Does not set common properties, that is done in {@link failureToError}. */ failureToErrorInner(failure, payloadConverter) { if (failure.applicationFailureInfo) { return new failure_1.ApplicationFailure(failure.message ?? undefined, failure.applicationFailureInfo.type, Boolean(failure.applicationFailureInfo.nonRetryable), (0, payload_converter_1.arrayFromPayloads)(payloadConverter, failure.applicationFailureInfo.details?.payloads), this.optionalFailureToOptionalError(failure.cause, payloadConverter), undefined, (0, failure_1.decodeApplicationFailureCategory)(failure.applicationFailureInfo.category)); } if (failure.serverFailureInfo) { return new failure_1.ServerFailure(failure.message ?? undefined, Boolean(failure.serverFailureInfo.nonRetryable), this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } if (failure.timeoutFailureInfo) { return new failure_1.TimeoutFailure(failure.message ?? undefined, (0, payload_converter_1.fromPayloadsAtIndex)(payloadConverter, 0, failure.timeoutFailureInfo.lastHeartbeatDetails?.payloads), (0, failure_1.decodeTimeoutType)(failure.timeoutFailureInfo.timeoutType)); } if (failure.terminatedFailureInfo) { return new failure_1.TerminatedFailure(failure.message ?? undefined, this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } if (failure.canceledFailureInfo) { return new failure_1.CancelledFailure(failure.message ?? undefined, (0, payload_converter_1.arrayFromPayloads)(payloadConverter, failure.canceledFailureInfo.details?.payloads), this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } if (failure.resetWorkflowFailureInfo) { return new failure_1.ApplicationFailure(failure.message ?? undefined, 'ResetWorkflow', false, (0, payload_converter_1.arrayFromPayloads)(payloadConverter, failure.resetWorkflowFailureInfo.lastHeartbeatDetails?.payloads), this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } if (failure.childWorkflowExecutionFailureInfo) { const { namespace, workflowType, workflowExecution, retryState } = failure.childWorkflowExecutionFailureInfo; if (!(workflowType?.name && workflowExecution)) { throw new TypeError('Missing attributes on childWorkflowExecutionFailureInfo'); } return new failure_1.ChildWorkflowFailure(namespace ?? undefined, workflowExecution, workflowType.name, (0, failure_1.decodeRetryState)(retryState), this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } if (failure.activityFailureInfo) { if (!failure.activityFailureInfo.activityType?.name) { throw new TypeError('Missing activityType?.name on activityFailureInfo'); } return new failure_1.ActivityFailure(failure.message ?? undefined, failure.activityFailureInfo.activityType.name, failure.activityFailureInfo.activityId ?? undefined, (0, failure_1.decodeRetryState)(failure.activityFailureInfo.retryState), failure.activityFailureInfo.identity ?? undefined, this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } if (failure.nexusHandlerFailureInfo) { let retryableOverride = undefined; const retryBehavior = decodeNexusHandlerErrorRetryBehavior(failure.nexusHandlerFailureInfo.retryBehavior); switch (retryBehavior) { case 'RETRYABLE': retryableOverride = true; break; case 'NON_RETRYABLE': retryableOverride = false; break; } const rawErrorType = failure.nexusHandlerFailureInfo.type || ''; const resolvedType = Object.hasOwn(nexus.HandlerErrorType, rawErrorType) ? nexus.HandlerErrorType[rawErrorType] : 'UNKNOWN'; return new nexus.HandlerError(resolvedType, failure.message ?? 'Nexus handler error', { cause: this.optionalFailureToOptionalError(failure.cause, payloadConverter), retryableOverride, rawErrorType, originalFailure: this.temporalFailureToNexusFailure(failure), }); } if (failure.nexusOperationExecutionFailureInfo) { return new failure_1.NexusOperationFailure( // TODO(nexus/error): Maybe set a default message here, once we've decided on error handling. failure.message ?? undefined, failure.nexusOperationExecutionFailureInfo.scheduledEventId?.toNumber(), // We assume these will always be set or gracefully set to empty strings. failure.nexusOperationExecutionFailureInfo.endpoint ?? '', failure.nexusOperationExecutionFailureInfo.service ?? '', failure.nexusOperationExecutionFailureInfo.operation ?? '', failure.nexusOperationExecutionFailureInfo.operationToken ?? undefined, this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } return new failure_1.TemporalFailure(failure.message ?? undefined, this.optionalFailureToOptionalError(failure.cause, payloadConverter)); } failureToError(failure, payloadConverter) { if (failure.encodedAttributes) { const attrs = payloadConverter.fromPayload(failure.encodedAttributes); // Don't apply encodedAttributes unless they conform to an expected schema if (typeof attrs === 'object' && attrs !== null) { const { message, stack_trace } = attrs; // Avoid mutating the argument failure = { ...failure }; if (typeof message === 'string') { failure.message = message; } if (typeof stack_trace === 'string') { failure.stackTrace = stack_trace; } } } const err = this.failureToErrorInner(failure, payloadConverter); err.stack = failure.stackTrace ?? ''; if (err instanceof failure_1.TemporalFailure) { err.failure = failure; } return err; } errorToFailure(err, payloadConverter) { const failure = this.errorToFailureInner(err, payloadConverter); if (this.options.encodeCommonAttributes) { const { message, stackTrace } = failure; failure.message = 'Encoded failure'; failure.stackTrace = ''; failure.encodedAttributes = payloadConverter.toPayload({ message, stack_trace: stackTrace }); } return failure; } errorToFailureInner(err, payloadConverter) { if (err instanceof failure_1.TemporalFailure || err instanceof nexus.HandlerError) { if (err instanceof failure_1.TemporalFailure && err.failure) return err.failure; const base = { message: err.message, stackTrace: cutoffStackTrace(err.stack), cause: this.optionalErrorToOptionalFailure(err.cause, payloadConverter), source: failure_1.FAILURE_SOURCE, }; if (err instanceof failure_1.ActivityFailure) { return { ...base, activityFailureInfo: { ...err, retryState: (0, failure_1.encodeRetryState)(err.retryState), activityType: { name: err.activityType }, }, }; } if (err instanceof failure_1.ChildWorkflowFailure) { return { ...base, childWorkflowExecutionFailureInfo: { ...err, retryState: (0, failure_1.encodeRetryState)(err.retryState), workflowExecution: err.execution, workflowType: { name: err.workflowType }, }, }; } if (err instanceof failure_1.ApplicationFailure) { return { ...base, applicationFailureInfo: { type: err.type, nonRetryable: err.nonRetryable, details: err.details && err.details.length ? { payloads: (0, payload_converter_1.toPayloads)(payloadConverter, ...err.details) } : undefined, nextRetryDelay: (0, time_1.msOptionalToTs)(err.nextRetryDelay), category: (0, failure_1.encodeApplicationFailureCategory)(err.category), }, }; } if (err instanceof failure_1.CancelledFailure) { return { ...base, canceledFailureInfo: { details: err.details && err.details.length ? { payloads: (0, payload_converter_1.toPayloads)(payloadConverter, ...err.details) } : undefined, }, }; } if (err instanceof failure_1.TimeoutFailure) { return { ...base, timeoutFailureInfo: { timeoutType: (0, failure_1.encodeTimeoutType)(err.timeoutType), lastHeartbeatDetails: err.lastHeartbeatDetails ? { payloads: (0, payload_converter_1.toPayloads)(payloadConverter, err.lastHeartbeatDetails) } : undefined, }, }; } if (err instanceof failure_1.ServerFailure) { return { ...base, serverFailureInfo: { nonRetryable: err.nonRetryable }, }; } if (err instanceof failure_1.TerminatedFailure) { return { ...base, terminatedFailureInfo: {}, }; } if (err instanceof nexus.HandlerError) { if (err.originalFailure) { return this.nexusFailureToTemporalFailure(err.originalFailure, err.retryable); } else { let retryBehavior = undefined; switch (err.retryableOverride) { case true: retryBehavior = encodeNexusHandlerErrorRetryBehavior('RETRYABLE'); break; case false: retryBehavior = encodeNexusHandlerErrorRetryBehavior('NON_RETRYABLE'); break; } return { ...base, nexusHandlerFailureInfo: { type: err.type, retryBehavior, }, }; } } if (err instanceof failure_1.NexusOperationFailure) { return { ...base, nexusOperationExecutionFailureInfo: { scheduledEventId: err.scheduledEventId ? long_1.default.fromNumber(err.scheduledEventId) : undefined, endpoint: err.endpoint, service: err.service, operation: err.operation, operationToken: err.operationToken, }, }; } // Just a TemporalFailure return base; } const base = { source: failure_1.FAILURE_SOURCE, }; if ((0, type_helpers_1.isError)(err)) { return { ...base, message: String(err.message ?? ''), stackTrace: cutoffStackTrace(err.stack), cause: this.optionalErrorToOptionalFailure(err.cause, payloadConverter), }; } const recommendation = ` [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]`; if (typeof err === 'string') { return { ...base, message: err + recommendation }; } if (typeof err === 'object') { let message = ''; try { message = JSON.stringify(err); } catch (_err) { message = String(err); } return { ...base, message: message + recommendation }; } return { ...base, message: String(err) + recommendation }; } /** * Converts a Failure proto message to a JS Error object if defined or returns undefined. */ optionalFailureToOptionalError(failure, payloadConverter) { return failure ? this.failureToError(failure, payloadConverter) : undefined; } /** * Converts an error to a Failure proto message if defined or returns undefined */ optionalErrorToOptionalFailure(err, payloadConverter) { return err ? this.errorToFailure(err, payloadConverter) : undefined; } nexusFailureToTemporalFailure(failure, retryable) { if (failure.metadata?.type === 'temporal.api.failure.v1.Failure') { if (failure.details == null) { throw new TypeError("missing details for Nexus Failure of type 'temporal.api.failure.v1.Failure'"); } return failure.details; } else { const temporalFailure = {}; temporalFailure.applicationFailureInfo = { type: 'NexusFailure', nonRetryable: !retryable, details: { payloads: [ { metadata: { encoding: (0, encoding_1.encode)('json/plain') }, data: (0, encoding_1.encode)(JSON.stringify({ ...failure, message: '' })), }, ], }, }; temporalFailure.message = failure.message; temporalFailure.stackTrace = failure.stackTrace ?? ''; return temporalFailure; } } temporalFailureToNexusFailure(failure) { return { message: failure.message ?? '', metadata: { type: 'temporal.api.failure.v1.Failure' }, // Store the full ProtoFailure as the Nexus failure details so it can be round-tripped // losslessly back to a ProtoFailure via nexusFailureToTemporalFailure. details: { ...failure }, }; } } exports.DefaultFailureConverter = DefaultFailureConverter; //# sourceMappingURL=failure-converter.js.map