UNPKG

nestjs-temporal-core

Version:

Complete NestJS integration for Temporal.io with auto-discovery, declarative scheduling, enhanced monitoring, and enterprise-ready features

301 lines 13.3 kB
"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 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.debug(`Starting workflow '${workflowType}' with ID: ${workflowId}`); const handle = await this.client.workflow.start(workflowType, { workflowId, taskQueue, args, }); this.logger.info(`Started workflow: ${workflowType} [${workflowId}] on queue: ${taskQueue}`); return { ...handle, handle }; } catch (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(`Attempt ${attempt}/${maxRetries} failed for workflow '${workflowType}': ${message}. Retrying in ${retryDelay}ms...`); this.logger.debug(`Error details for retry decision:`, error); await this.sleep(retryDelay); continue; } this.logger.error(`Failed to start workflow '${workflowType}': ${message}`); this.logger.error('Full error object:', error); this.logger.debug(`Error details - Workflow: ${workflowType}, ID: ${workflowId}, Queue: ${taskQueue}, Retryable: ${isRetryableError}, Attempt: ${attempt}`); 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.debug(`Retrieved workflow handle: ${workflowId}${runId ? ` (run: ${runId})` : ''}`); return handle; } catch (error) { const message = this.extractErrorMessage(error); this.logger.error(`Failed to get workflow handle for ${workflowId}: ${message}`, 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) { const message = this.extractErrorMessage(error); this.logger.error(`Failed to terminate workflow ${workflowId}: ${message}`, error); throw new Error(`Failed to terminate workflow ${workflowId}: ${message}`); } } async cancelWorkflow(workflowId, runId) { try { const handle = await this.getWorkflowHandle(workflowId, runId); await handle.cancel(); this.logger.info(`Cancelled workflow: ${workflowId}`); } catch (error) { const message = this.extractErrorMessage(error); this.logger.error(`Failed to cancel workflow ${workflowId}: ${message}`, error); throw new Error(`Failed to cancel workflow ${workflowId}: ${message}`); } } 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) { const message = this.extractErrorMessage(error); this.logger.error(`Failed to send signal '${signalName}' to workflow ${workflowId}: ${message}`, error); throw new Error(`Failed to send signal '${signalName}' to workflow ${workflowId}: ${message}`); } } async signalWorkflowHandle(handle, signalName, args) { try { await handle.signal(signalName, ...(args || [])); this.logger.debug(`Sent signal '${signalName}' to workflow handle`); } catch (error) { this.logger.error(`Failed to send signal '${signalName}': ${this.extractErrorMessage(error)}`, error); throw error; } } 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) { const message = this.extractErrorMessage(error); this.logger.error(`Failed to query '${queryName}' on workflow ${workflowId}: ${message}`, error); throw new Error(`Failed to query '${queryName}' on workflow ${workflowId}: ${message}`); } } async queryWorkflowHandle(handle, queryName, args) { try { const result = await handle.query(queryName, ...(args || [])); this.logger.debug(`Queried '${queryName}' from workflow handle`); return result; } catch (error) { this.logger.error(`Failed to query '${queryName}': ${this.extractErrorMessage(error)}`, error); throw error; } } async getWorkflowResult(workflowId, runId) { try { const handle = await this.getWorkflowHandle(workflowId, runId); const result = await handle.result(); this.logger.debug(`Retrieved result from workflow: ${workflowId}`); return result; } catch (error) { this.logger.error(`Failed to get result from ${workflowId}: ${this.extractErrorMessage(error)}`, 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