nestjs-temporal-core
Version:
Complete NestJS integration for Temporal.io with auto-discovery, declarative scheduling, enhanced monitoring, and enterprise-ready features
517 lines • 23.3 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var TemporalClientService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TemporalClientService = void 0;
const common_1 = require("@nestjs/common");
const client_1 = require("@temporalio/client");
const constants_1 = require("../constants");
const logger_1 = require("../utils/logger");
let TemporalClientService = TemporalClientService_1 = class TemporalClientService {
constructor(temporalClient, options) {
this.temporalClient = temporalClient;
this.options = options;
this.client = null;
this.isInitialized = false;
this.lastHealthCheck = null;
this.healthCheckInterval = 30000;
this.logger = (0, logger_1.createLogger)(TemporalClientService_1.name, {
enableLogger: options.enableLogger,
logLevel: options.logLevel,
});
}
async onModuleInit() {
try {
this.client = this.temporalClient;
if (this.client) {
this.isInitialized = true;
this.logger.info('Temporal client service initialized successfully');
this.logger.debug(`Client namespace: ${this.options?.connection?.namespace || 'default'}`);
await this.performHealthCheck();
}
else {
this.logger.warn('No Temporal client available - running in client-less mode');
}
}
catch (error) {
this.logger.error('Failed to initialize Temporal client service', error);
throw error;
}
}
async startWorkflow(workflowType, args = [], options) {
this.ensureClientAvailable();
if (options?.workflowId !== undefined) {
this.validateWorkflowId(options.workflowId);
}
const workflowId = options?.workflowId || this.generateWorkflowId(workflowType);
const taskQueue = options?.taskQueue || this.options.taskQueue || 'default';
const maxRetries = 3;
const baseRetryDelay = 1000;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
if (attempt > 1) {
this.logger.debug(`Performing health check before retry attempt ${attempt}`);
await this.performHealthCheck();
}
this.logger.verbose(`Starting workflow '${workflowType}' [${workflowId}] on queue '${taskQueue}'`);
const workflowOptions = {
workflowId,
taskQueue,
args: [...args],
...(options?.workflowExecutionTimeout && {
workflowExecutionTimeout: options.workflowExecutionTimeout,
}),
...(options?.workflowRunTimeout && {
workflowRunTimeout: options.workflowRunTimeout,
}),
...(options?.workflowTaskTimeout && {
workflowTaskTimeout: options.workflowTaskTimeout,
}),
...(options?.typedSearchAttributes && {
typedSearchAttributes: options.typedSearchAttributes,
}),
...(options?.searchAttributes &&
!options.typedSearchAttributes && {
typedSearchAttributes: options.searchAttributes,
}),
...(options?.memo && {
memo: options.memo,
}),
...(options?.workflowIdReusePolicy !== undefined && {
workflowIdReusePolicy: options.workflowIdReusePolicy,
}),
...(options?.retry && {
retry: options.retry,
}),
...(options?.retryPolicy &&
!options.retry && {
retry: options.retryPolicy,
}),
...(options?.followRuns !== undefined && { followRuns: options.followRuns }),
...(options?.startDelay && { startDelay: options.startDelay }),
...(options?.workflowIdConflictPolicy !== undefined && {
workflowIdConflictPolicy: options.workflowIdConflictPolicy,
}),
...(options?.versioningOverride && {
versioningOverride: options.versioningOverride,
}),
};
const handle = await this.client.workflow.start(workflowType, workflowOptions);
this.logger.info(`Started workflow '${workflowType}' [${workflowId}] on '${taskQueue}'`);
return { ...handle, handle };
}
catch (error) {
if (error instanceof client_1.WorkflowExecutionAlreadyStartedError) {
throw error;
}
const message = this.extractErrorMessage(error);
const isRetryableError = this.isRetryableError(error, message);
if (isRetryableError && attempt < maxRetries) {
const retryDelay = baseRetryDelay * Math.pow(2, attempt - 1);
this.logger.warn(`Workflow '${workflowType}' start failed (attempt ${attempt}/${maxRetries}), retrying in ${retryDelay}ms: ${message}`);
await this.sleep(retryDelay);
continue;
}
this.logger.error(`Failed to start workflow '${workflowType}' [${workflowId}] on queue '${taskQueue}' after ${attempt} attempt(s)`, error);
throw new Error(`Failed to start workflow '${workflowType}': ${message}`);
}
}
throw new Error(`Failed to start workflow '${workflowType}' after ${maxRetries} attempts`);
}
async getWorkflowHandle(workflowId, runId) {
this.ensureClientAvailable();
try {
const handle = await this.client.workflow.getHandle(workflowId, runId);
this.logger.verbose(`Retrieved workflow handle for '${workflowId}'${runId ? ` (run: ${runId})` : ''}`);
return handle;
}
catch (error) {
const message = this.extractErrorMessage(error);
this.logger.error(`Failed to get workflow handle for '${workflowId}'`, error);
throw new Error(`Failed to get workflow handle for ${workflowId}: ${message}`);
}
}
async terminateWorkflow(workflowId, reason, runId) {
try {
const handle = await this.getWorkflowHandle(workflowId, runId);
await handle.terminate(reason);
this.logger.info(`Terminated workflow '${workflowId}'${reason ? `: ${reason}` : ''}`);
}
catch (error) {
this.logger.error(`Failed to terminate workflow '${workflowId}'`, error);
throw new Error(`Failed to terminate workflow ${workflowId}: ${this.extractErrorMessage(error)}`);
}
}
async cancelWorkflow(workflowId, runId) {
try {
const handle = await this.getWorkflowHandle(workflowId, runId);
await handle.cancel();
this.logger.info(`Cancelled workflow '${workflowId}'`);
}
catch (error) {
this.logger.error(`Failed to cancel workflow '${workflowId}'`, error);
throw new Error(`Failed to cancel workflow ${workflowId}: ${this.extractErrorMessage(error)}`);
}
}
async signalWorkflow(workflowId, signalName, args, runId) {
try {
const handle = await this.getWorkflowHandle(workflowId, runId);
await handle.signal(signalName, ...(args || []));
this.logger.debug(`Sent signal '${signalName}' to workflow '${workflowId}'`);
}
catch (error) {
this.logger.error(`Failed to send signal '${signalName}' to workflow '${workflowId}'`, error);
throw new Error(`Failed to send signal '${signalName}' to workflow ${workflowId}: ${this.extractErrorMessage(error)}`);
}
}
async signalWorkflowHandle(handle, signalName, args) {
try {
await handle.signal(signalName, ...(args || []));
this.logger.verbose(`Sent signal '${signalName}' to workflow`);
}
catch (error) {
this.logger.error(`Failed to send signal '${signalName}'`, error);
throw error;
}
}
async signalWithStart(workflowType, signalName, signalArgs, workflowArgs, options) {
this.ensureClientAvailable();
const workflowId = options?.workflowId || this.generateWorkflowId(workflowType);
const taskQueue = options?.taskQueue || this.options.taskQueue || 'default';
try {
this.logger.verbose(`Signal-with-starting workflow '${workflowType}' [${workflowId}] signal='${signalName}'`);
const handle = await this.client.workflow.signalWithStart(workflowType, {
workflowId,
taskQueue,
args: [...workflowArgs],
signal: signalName,
signalArgs: [...signalArgs],
...(options?.workflowExecutionTimeout && {
workflowExecutionTimeout: options.workflowExecutionTimeout,
}),
...(options?.workflowRunTimeout && {
workflowRunTimeout: options.workflowRunTimeout,
}),
...(options?.workflowTaskTimeout && {
workflowTaskTimeout: options.workflowTaskTimeout,
}),
...(options?.memo && { memo: options.memo }),
...(options?.workflowIdReusePolicy && {
workflowIdReusePolicy: options.workflowIdReusePolicy,
}),
});
this.logger.info(`Signal-with-started workflow '${workflowType}' [${workflowId}] signal='${signalName}'`);
return { ...handle, handle };
}
catch (error) {
const message = this.extractErrorMessage(error);
this.logger.error(`Failed to signalWithStart workflow '${workflowType}' [${workflowId}]`, error);
throw new Error(`Failed to signalWithStart workflow '${workflowType}': ${message}`);
}
}
async queryWorkflow(workflowId, queryName, args, runId) {
try {
const handle = await this.getWorkflowHandle(workflowId, runId);
const result = await handle.query(queryName, ...(args || []));
this.logger.debug(`Queried '${queryName}' from workflow '${workflowId}'`);
return result;
}
catch (error) {
this.logger.error(`Failed to query '${queryName}' on workflow '${workflowId}'`, error);
throw new Error(`Failed to query '${queryName}' on workflow ${workflowId}: ${this.extractErrorMessage(error)}`);
}
}
async queryWorkflowHandle(handle, queryName, args) {
try {
const result = await handle.query(queryName, ...(args || []));
this.logger.verbose(`Queried '${queryName}' from workflow`);
return result;
}
catch (error) {
this.logger.error(`Failed to query '${queryName}'`, error);
throw error;
}
}
async updateWorkflow(workflowId, updateName, args, runId) {
try {
const handle = await this.getWorkflowHandle(workflowId, runId);
const executeUpdate = handle.executeUpdate;
const result = await executeUpdate(updateName, { args: [...(args || [])] });
this.logger.debug(`Executed update '${updateName}' on workflow '${workflowId}'`);
return result;
}
catch (error) {
this.logger.error(`Failed to execute update '${updateName}' on workflow '${workflowId}'`, error);
throw new Error(`Failed to execute update '${updateName}' on workflow ${workflowId}: ${this.extractErrorMessage(error)}`);
}
}
async updateWorkflowHandle(handle, updateName, args) {
try {
const executeUpdate = handle.executeUpdate;
const result = await executeUpdate(updateName, { args: [...(args || [])] });
this.logger.verbose(`Executed update '${updateName}' on workflow`);
return result;
}
catch (error) {
this.logger.error(`Failed to execute update '${updateName}'`, error);
throw error;
}
}
async startUpdateWorkflow(workflowId, updateName, args, runId) {
try {
const handle = await this.getWorkflowHandle(workflowId, runId);
const startUpdate = handle.startUpdate;
const updateHandle = await startUpdate(updateName, {
args: [...(args || [])],
waitForStage: 'ACCEPTED',
});
this.logger.debug(`Started update '${updateName}' on workflow '${workflowId}'`);
return updateHandle;
}
catch (error) {
this.logger.error(`Failed to start update '${updateName}' on workflow '${workflowId}'`, error);
throw new Error(`Failed to start update '${updateName}' on workflow ${workflowId}: ${this.extractErrorMessage(error)}`);
}
}
async startUpdateWorkflowHandle(handle, updateName, args) {
try {
const startUpdate = handle.startUpdate;
const updateHandle = await startUpdate(updateName, {
args: [...(args || [])],
waitForStage: 'ACCEPTED',
});
this.logger.verbose(`Started update '${updateName}' on workflow`);
return updateHandle;
}
catch (error) {
this.logger.error(`Failed to start update '${updateName}'`, error);
throw error;
}
}
async completeActivity(taskTokenOrFullActivityId, result) {
this.ensureClientAvailable();
try {
await this.client.activity.complete(taskTokenOrFullActivityId, result);
this.logger.debug('Completed activity');
}
catch (error) {
this.logger.error('Failed to complete activity', error);
throw new Error(`Failed to complete activity: ${this.extractErrorMessage(error)}`);
}
}
async failActivity(taskTokenOrFullActivityId, err) {
this.ensureClientAvailable();
try {
await this.client.activity.fail(taskTokenOrFullActivityId, err);
this.logger.debug('Failed activity (reported to server)');
}
catch (error) {
this.logger.error('Failed to report activity failure', error);
throw new Error(`Failed to report activity failure: ${this.extractErrorMessage(error)}`);
}
}
async heartbeatActivity(taskTokenOrFullActivityId, details) {
this.ensureClientAvailable();
try {
await this.client.activity.heartbeat(taskTokenOrFullActivityId, details);
this.logger.verbose('Sent activity heartbeat');
}
catch (error) {
this.logger.error('Failed to send activity heartbeat', error);
throw new Error(`Failed to send activity heartbeat: ${this.extractErrorMessage(error)}`);
}
}
async reportActivityCancellation(taskTokenOrFullActivityId, details) {
this.ensureClientAvailable();
try {
await this.client.activity.reportCancellation(taskTokenOrFullActivityId, details);
this.logger.debug('Reported activity cancellation');
}
catch (error) {
this.logger.error('Failed to report activity cancellation', error);
throw new Error(`Failed to report activity cancellation: ${this.extractErrorMessage(error)}`);
}
}
async startStandaloneActivity(activityType, options) {
this.ensureClientAvailable();
try {
const handle = await this.client.activity.start(activityType, options);
this.logger.info(`Started standalone activity '${activityType}' [${options.id}]`);
return handle;
}
catch (error) {
this.logger.error(`Failed to start standalone activity '${activityType}'`, error);
throw new Error(`Failed to start standalone activity '${activityType}': ${this.extractErrorMessage(error)}`);
}
}
async executeStandaloneActivity(activityType, options) {
this.ensureClientAvailable();
try {
const result = await this.client.activity.execute(activityType, options);
this.logger.info(`Executed standalone activity '${activityType}' [${options.id}]`);
return result;
}
catch (error) {
this.logger.error(`Failed to execute standalone activity '${activityType}'`, error);
throw new Error(`Failed to execute standalone activity '${activityType}': ${this.extractErrorMessage(error)}`);
}
}
getStandaloneActivityHandle(activityId, runId) {
if (!this.client) {
throw new Error('Temporal client not initialized');
}
return this.client.activity.getHandle(activityId, runId);
}
listStandaloneActivities(query) {
if (!this.client) {
throw new Error('Temporal client not initialized');
}
return this.client.activity.list(query);
}
async countStandaloneActivities(query) {
this.ensureClientAvailable();
try {
return await this.client.activity.count(query);
}
catch (error) {
this.logger.error('Failed to count standalone activities', error);
throw new Error(`Failed to count standalone activities: ${this.extractErrorMessage(error)}`);
}
}
async getWorkflowResult(workflowId, runId) {
try {
const handle = await this.getWorkflowHandle(workflowId, runId);
const result = await handle.result();
this.logger.verbose(`Retrieved result from workflow '${workflowId}'`);
return result;
}
catch (error) {
this.logger.error(`Failed to get result from workflow '${workflowId}'`, error);
throw error;
}
}
isHealthy() {
if (!this.isInitialized || !this.client) {
return false;
}
const now = new Date();
if (!this.lastHealthCheck ||
now.getTime() - this.lastHealthCheck.getTime() > this.healthCheckInterval) {
this.performHealthCheck().catch((error) => {
this.logger.warn('Health check failed', error);
});
}
return Boolean(this.client.workflow);
}
getHealth() {
return { status: this.isHealthy() ? 'healthy' : 'unhealthy' };
}
getStatus() {
return {
available: this.client !== null,
healthy: this.isHealthy(),
initialized: this.isInitialized,
lastHealthCheck: this.lastHealthCheck,
namespace: this.options.connection?.namespace || 'default',
};
}
getRawClient() {
return this.client;
}
async performHealthCheck() {
if (!this.client) {
return;
}
try {
if (!this.client) {
throw new Error('Client is not initialized');
}
this.lastHealthCheck = new Date();
this.logger.debug('Client health check passed');
}
catch (error) {
this.logger.warn('Client health check failed', error);
throw error;
}
}
ensureClientAvailable() {
if (!this.client) {
throw new Error('Temporal client not initialized');
}
}
generateWorkflowId(workflowType) {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
const workflowId = `${workflowType}-${timestamp}-${random}`;
this.validateWorkflowId(workflowId);
return workflowId;
}
validateWorkflowId(workflowId) {
if (!workflowId || workflowId.trim() === '') {
throw new Error('Workflow ID cannot be empty');
}
if (workflowId.length > 1000) {
throw new Error(`Workflow ID too long (${workflowId.length} characters). Maximum length is 1000 characters`);
}
if (/[\n\r\t\u0000-\u001f\u007f]/.test(workflowId)) {
throw new Error('Workflow ID cannot contain newlines, tabs, or control characters');
}
}
extractErrorMessage(error) {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
return 'Unknown error';
}
isRetryableError(error, message) {
const gRpcErrorPatterns = [
'Unexpected error while making gRPC request',
'connection error',
'UNAVAILABLE',
'DEADLINE_EXCEEDED',
'RESOURCE_EXHAUSTED',
'INTERNAL',
'Service unavailable',
'Connection refused',
'Network error',
'timeout',
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
];
const messageMatch = gRpcErrorPatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));
if (error && typeof error === 'object' && 'code' in error) {
const grpcCode = error.code;
const retryableGrpcCodes = [1, 2, 4, 8, 10, 13, 14];
if (typeof grpcCode === 'number' && retryableGrpcCodes.includes(grpcCode)) {
return true;
}
}
return messageMatch;
}
async sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
exports.TemporalClientService = TemporalClientService;
exports.TemporalClientService = TemporalClientService = TemporalClientService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)(constants_1.TEMPORAL_CLIENT)),
__param(1, (0, common_1.Inject)(constants_1.TEMPORAL_MODULE_OPTIONS)),
__metadata("design:paramtypes", [Object, Object])
], TemporalClientService);
//# sourceMappingURL=temporal-client.service.js.map