UNPKG

nestjs-temporal-core

Version:

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

389 lines 15.9 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 TemporalWorkerManagerService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.TemporalWorkerManagerService = void 0; const common_1 = require("@nestjs/common"); const core_1 = require("@nestjs/core"); const worker_1 = require("@temporalio/worker"); const temporal_metadata_accessor_1 = require("./temporal-metadata.accessor"); const constants_1 = require("../constants"); const logger_1 = require("../utils/logger"); let TemporalWorkerManagerService = TemporalWorkerManagerService_1 = class TemporalWorkerManagerService { constructor(options, discoveryService, metadataAccessor) { this.options = options; this.discoveryService = discoveryService; this.metadataAccessor = metadataAccessor; this.worker = null; this.connection = null; this.isInitialized = false; this.isRunning = false; this.activities = {}; this.workerPromise = null; this.logger = (0, logger_1.createLogger)(TemporalWorkerManagerService_1.name); } async onModuleInit() { try { this.logger.log('Initializing Temporal worker...'); await this.initializeWorker(); this.isInitialized = true; this.logger.log('Temporal worker initialization completed'); } catch (error) { this.lastError = error?.message || 'Unknown initialization error'; this.logger.error('Error during worker initialization', error?.stack || error); if (this.options?.allowWorkerFailure !== false) { this.logger.warn('Continuing application startup without Temporal worker'); } else { throw error; } } } async onApplicationBootstrap() { if (this.options?.autoStart === false || !this.worker) { this.logger.debug('Worker auto-start disabled or worker not initialized'); return; } setImmediate(() => { this.startWorkerInBackground(); }); } async onModuleDestroy() { await this.shutdown(); } startWorkerInBackground() { if (!this.worker || this.isRunning) { return; } this.logger.log(`Starting worker for task queue: ${this.options?.taskQueue} in background`); this.workerPromise = this.runWorkerLoop().catch((error) => { this.isRunning = false; this.lastError = error?.message || 'Unknown worker error'; this.logger.error('Worker crashed', error?.stack || error); if (this.options?.autoRestart !== false) { setTimeout(() => { this.logger.log('Attempting to restart worker...'); this.startWorkerInBackground(); }, 5000); } return Promise.resolve(); }); this.workerPromise.catch(() => { }); } async runWorkerLoop() { if (!this.worker) { throw new Error(constants_1.ERRORS.WORKER_NOT_INITIALIZED); } this.isRunning = true; this.startedAt = new Date(); this.lastError = undefined; this.logger.log('Worker started successfully'); try { await this.worker.run(); } catch (error) { this.isRunning = false; this.lastError = error?.message || 'Worker execution error'; this.logger.error('Worker execution failed', error?.stack || error); throw error; } finally { this.isRunning = false; this.logger.log('Worker execution completed'); } } async initializeWorker() { this.validateConfiguration(); this.activities = await this.discoverActivities(); await this.createConnection(); await this.createWorker(); this.logWorkerConfiguration(); } validateConfiguration() { const taskQueue = this.options?.taskQueue; if (!taskQueue) { throw new Error(constants_1.ERRORS.MISSING_TASK_QUEUE); } const workflowsPath = this.options?.workflowsPath; const workflowBundle = this.options?.workflowBundle; const hasWorkflowsPath = Boolean(workflowsPath); const hasWorkflowBundle = Boolean(workflowBundle); if (hasWorkflowsPath && hasWorkflowBundle) { throw new Error('Cannot specify both workflowsPath and workflowBundle'); } if (hasWorkflowBundle) { this.logger.debug('Using pre-bundled workflows (recommended for production)'); } else if (hasWorkflowsPath) { this.logger.debug('Using workflows from filesystem path (recommended for development)'); } else { this.logger.debug('Worker configured for activities only (no workflows)'); } } async createConnection() { const connection = this.options?.connection; const connectionOptions = { address: connection?.address || 'localhost:7233', tls: connection?.tls, }; if (connection?.apiKey) { connectionOptions.metadata = { ...(connection?.metadata || {}), authorization: `Bearer ${connection.apiKey}`, }; } this.logger.debug(`Connecting to Temporal server at ${connectionOptions.address}`); this.connection = await worker_1.NativeConnection.connect(connectionOptions); this.logger.debug('Temporal connection established'); } async createWorker() { if (!this.connection) { throw new Error('Connection not established'); } const workerOptions = this.buildWorkerOptions(); const connection = this.options?.connection; const namespace = connection?.namespace || constants_1.DEFAULT_NAMESPACE; this.worker = await worker_1.Worker.create({ connection: this.connection, namespace, taskQueue: this.options?.taskQueue, ...workerOptions, }); this.logger.log(`Worker created for queue: ${this.options?.taskQueue} in namespace: ${namespace}`); } buildWorkerOptions() { const baseOptions = { taskQueue: this.options?.taskQueue, activities: this.activities, }; const workflowBundle = this.options?.workflowBundle; const workflowsPath = this.options?.workflowsPath; if (workflowBundle) { baseOptions.workflowBundle = workflowBundle; } else if (workflowsPath) { baseOptions.workflowsPath = workflowsPath; } const defaultOptions = this.getEnvironmentDefaults(); const userOptions = this.options?.workerOptions || {}; return { ...baseOptions, ...defaultOptions, ...userOptions, }; } getEnvironmentDefaults() { const env = process.env.NODE_ENV || 'development'; switch (env) { case 'production': return constants_1.WORKER_PRESETS.PRODUCTION_BALANCED; case 'development': return constants_1.WORKER_PRESETS.DEVELOPMENT; default: return { maxConcurrentActivityTaskExecutions: 20, maxConcurrentWorkflowTaskExecutions: 10, maxConcurrentLocalActivityExecutions: 20, reuseV8Context: true, }; } } async discoverActivities() { const activities = {}; const providers = this.discoveryService.getProviders(); const activityProviders = providers.filter((wrapper) => { const { instance, metatype } = wrapper; const targetClass = instance?.constructor || metatype; if (!targetClass) return false; const activityClasses = this.options?.activityClasses; if (activityClasses?.length) { return (activityClasses.includes(targetClass) && this.metadataAccessor.isActivity(targetClass)); } return this.metadataAccessor.isActivity(targetClass); }); this.logger.log(`Found ${activityProviders.length} activity providers`); for (const wrapper of activityProviders) { const { instance } = wrapper; if (!instance) continue; try { const className = instance.constructor.name; this.logger.debug(`Processing activity class: ${className}`); const validation = this.metadataAccessor.validateActivityClass(instance.constructor); if (!validation.isValid) { this.logger.warn(`Activity class ${className} has issues: ${validation.issues.join(', ')}`); continue; } const activityMethods = this.metadataAccessor.extractActivityMethods(instance); for (const [activityName, method] of activityMethods.entries()) { activities[activityName] = method; this.logger.debug(`Registered activity: ${className}.${activityName}`); } } catch (error) { this.logger.error(`Failed to process activity class ${instance.constructor.name}:`, error?.stack || error); } } const activityCount = Object.keys(activities).length; this.logger.log(`Registered ${activityCount} activity methods in total`); return activities; } logWorkerConfiguration() { const connection = this.options?.connection; const workflowBundle = this.options?.workflowBundle; const workflowsPath = this.options?.workflowsPath; const config = { taskQueue: this.options?.taskQueue, namespace: connection?.namespace || constants_1.DEFAULT_NAMESPACE, workflowSource: workflowBundle ? 'bundle' : workflowsPath ? 'filesystem' : 'none', activitiesCount: Object.keys(this.activities).length, autoStart: this.options?.autoStart !== false, environment: process.env.NODE_ENV || 'development', }; this.logger.log('Worker configuration summary:'); this.logger.debug(JSON.stringify(config, null, 2)); } async shutdown() { this.logger.log('Shutting down Temporal worker...'); if (this.worker && this.isRunning) { try { await this.worker.shutdown(); this.isRunning = false; this.logger.log('Worker shut down successfully'); } catch (error) { this.logger.error('Error during worker shutdown', error?.stack); } finally { this.worker = null; } } if (this.workerPromise) { try { await Promise.race([ this.workerPromise, new Promise((resolve) => setTimeout(resolve, 5000)), ]); } catch (error) { this.logger.debug('Worker promise completed with error during shutdown:', error?.message); } finally { this.workerPromise = null; } } if (this.connection) { try { await this.connection.close(); this.logger.log('Connection closed successfully'); } catch (error) { this.logger.error('Error during connection close', error?.stack); } finally { this.connection = null; } } this.isInitialized = false; this.startedAt = undefined; } getWorker() { return this.worker; } getConnection() { return this.connection; } isWorkerRunning() { return this.isRunning; } isWorkerInitialized() { return this.isInitialized; } getWorkerStatus() { const uptime = this.startedAt ? Date.now() - this.startedAt.getTime() : undefined; const connection = this.options?.connection; const workflowBundle = this.options?.workflowBundle; const workflowsPath = this.options?.workflowsPath; return { isInitialized: this.isInitialized, isRunning: this.isRunning, isHealthy: this.isInitialized && !this.lastError && this.connection !== null, taskQueue: this.options?.taskQueue || 'unknown', namespace: connection?.namespace || constants_1.DEFAULT_NAMESPACE, workflowSource: workflowBundle ? 'bundle' : workflowsPath ? 'filesystem' : 'none', activitiesCount: Object.keys(this.activities).length, lastError: this.lastError, startedAt: this.startedAt, uptime, }; } getRegisteredActivities() { return Object.keys(this.activities); } async restartWorker() { this.logger.log('Restarting Temporal worker...'); await this.shutdown(); try { await this.initializeWorker(); this.isInitialized = true; this.logger.log('Worker restarted successfully'); if (this.options?.autoStart !== false) { this.startWorkerInBackground(); } } catch (error) { this.lastError = error?.message || 'Unknown restart error'; this.logger.error('Error during worker restart', error?.stack || error); throw error; } } async healthCheck() { const status = this.getWorkerStatus(); const activities = this.getRegisteredActivities(); let healthStatus; if (!status.isInitialized) { healthStatus = 'unhealthy'; } else if (status.lastError) { healthStatus = 'degraded'; } else if (status.isHealthy) { healthStatus = 'healthy'; } else { healthStatus = 'degraded'; } return { status: healthStatus, details: status, activities: { total: activities.length, registered: activities, }, }; } }; exports.TemporalWorkerManagerService = TemporalWorkerManagerService; exports.TemporalWorkerManagerService = TemporalWorkerManagerService = TemporalWorkerManagerService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_1.Inject)(constants_1.TEMPORAL_MODULE_OPTIONS)), __metadata("design:paramtypes", [Object, core_1.DiscoveryService, temporal_metadata_accessor_1.TemporalMetadataAccessor]) ], TemporalWorkerManagerService); //# sourceMappingURL=temporal-worker-manager.service.js.map