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
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 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