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