UNPKG

@pulzar/core

Version:

Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support

1,022 lines 34.1 kB
import { EventEmitter } from "events"; import { logger } from "../utils/logger"; const cronParser = require("cron-parser"); export class TaskAdapter extends EventEmitter { } export class TaskScheduler extends EventEmitter { adapter; tasks = new Map(); cronJobs = new Map(); queues = new Set(); options; connected = false; constructor(options) { super(); const defaultOptions = { adapter: "memory", redis: { host: "localhost", port: 6379, db: 0, }, postgres: { connectionString: "postgresql://localhost:5432/tasks", }, defaultRetries: 3, defaultTimeout: 30000, enableMetrics: true, }; this.options = { ...defaultOptions, ...options, }; this.adapter = this.createAdapter(); } /** * Create adapter instance based on configuration */ createAdapter() { switch (this.options.adapter) { case "memory": return new MemoryTaskAdapter(); case "bullmq": return new BullMQAdapter(this.options.redis); case "pgboss": return new PgBossAdapter(this.options.postgres); default: throw new Error(`Unsupported task adapter: ${this.options.adapter}`); } } /** * Initialize task scheduler */ async initialize() { if (this.connected) { throw new Error("TaskScheduler already initialized"); } logger.info("Initializing task scheduler", { adapter: this.options.adapter, tasks: this.tasks.size, }); try { await this.adapter.connect(); this.connected = true; // Setup cron jobs await this.setupCronJobs(); // Setup queue processors await this.setupQueueProcessors(); this.emit("initialized"); logger.info("Task scheduler initialized", { cronJobs: this.cronJobs.size, queues: this.queues.size, }); } catch (error) { logger.error("Failed to initialize task scheduler", { error }); throw error; } } /** * Register a task */ registerTask(metadata, handler) { if (this.tasks.has(metadata.name)) { throw new Error(`Task "${metadata.name}" is already registered`); } const taskConfig = { retries: this.options.defaultRetries, timeout: this.options.defaultTimeout, enabled: true, ...metadata, handler, }; this.tasks.set(metadata.name, taskConfig); if (metadata.queue) { this.queues.add(metadata.queue); } logger.debug("Task registered", { name: metadata.name, cron: metadata.cron, queue: metadata.queue, }); } /** * Setup cron jobs */ async setupCronJobs() { for (const [name, task] of this.tasks) { if (task.cron && task.enabled) { try { const interval = cronParser.parseExpression(task.cron); const nextRun = interval.next().toDate(); const delay = nextRun.getTime() - Date.now(); const timeout = setTimeout(async () => { await this.executeCronTask(name, task); }, delay); this.cronJobs.set(name, timeout); logger.info("Cron job scheduled", { task: name, cron: task.cron, nextRun: nextRun.toISOString(), }); } catch (error) { logger.error("Failed to schedule cron job", { task: name, cron: task.cron, error, }); } } } } /** * Execute cron task and reschedule */ async executeCronTask(name, task) { try { logger.debug("Executing cron task", { task: name }); await Promise.race([ task.handler(), new Promise((_, reject) => setTimeout(() => reject(new Error("Task timeout")), task.timeout)), ]); logger.info("Cron task completed", { task: name }); this.emit("taskCompleted", { name, type: "cron" }); } catch (error) { logger.error("Cron task failed", { task: name, error }); this.emit("taskFailed", { name, type: "cron", error }); } // Reschedule for next execution if (task.cron && task.enabled) { try { const interval = cronParser.parseExpression(task.cron); const nextRun = interval.next().toDate(); const delay = nextRun.getTime() - Date.now(); const timeout = setTimeout(async () => { await this.executeCronTask(name, task); }, delay); this.cronJobs.set(name, timeout); } catch (error) { logger.error("Failed to reschedule cron task", { task: name, error }); } } } /** * Setup queue processors */ async setupQueueProcessors() { for (const queueName of this.queues) { await this.adapter.processQueue(queueName, async (data) => { const task = Array.from(this.tasks.values()).find((t) => t.queue === queueName); if (task) { await task.handler(data); } }); } } /** * Add job to queue */ async addJob(queueName, data, options = {}) { if (!this.connected) { throw new Error("TaskScheduler not connected"); } const job = { id: this.generateJobId(), name: queueName, data, options: { retries: this.options.defaultRetries, timeout: this.options.defaultTimeout, ...options, }, attempts: 0, }; await this.adapter.scheduleJob(job); logger.debug("Job added to queue", { jobId: job.id, queue: queueName, priority: options.priority, }); this.emit("jobAdded", job); return job.id; } /** * Get queue information */ async getQueueInfo(queueName) { if (!this.connected) { throw new Error("TaskScheduler not connected"); } return this.adapter.getQueueInfo(queueName); } /** * Remove job from queue */ async removeJob(jobId) { if (!this.connected) { throw new Error("TaskScheduler not connected"); } const removed = await this.adapter.removeJob(jobId); if (removed) { logger.info("Job removed", { jobId }); this.emit("jobRemoved", { jobId }); } return removed; } /** * Retry failed job */ async retryJob(jobId) { if (!this.connected) { throw new Error("TaskScheduler not connected"); } const retried = await this.adapter.retryJob(jobId); if (retried) { logger.info("Job retried", { jobId }); this.emit("jobRetried", { jobId }); } return retried; } /** * Pause queue processing */ async pauseQueue(queueName) { if (!this.connected) { throw new Error("TaskScheduler not connected"); } await this.adapter.pauseQueue(queueName); logger.info("Queue paused", { queue: queueName }); this.emit("queuePaused", { queueName }); } /** * Resume queue processing */ async resumeQueue(queueName) { if (!this.connected) { throw new Error("TaskScheduler not connected"); } await this.adapter.resumeQueue(queueName); logger.info("Queue resumed", { queue: queueName }); this.emit("queueResumed", { queueName }); } /** * Enable/disable a task */ setTaskEnabled(taskName, enabled) { const task = this.tasks.get(taskName); if (!task) { throw new Error(`Task "${taskName}" not found`); } task.enabled = enabled; if (!enabled && task.cron) { // Cancel cron job const timeout = this.cronJobs.get(taskName); if (timeout) { clearTimeout(timeout); this.cronJobs.delete(taskName); } } else if (enabled && task.cron) { // Reschedule cron job this.setupCronJobForTask(taskName, task); } logger.info("Task enabled state changed", { task: taskName, enabled }); } /** * Setup cron job for a specific task */ setupCronJobForTask(name, task) { if (!task.cron) return; try { const interval = cronParser.parseExpression(task.cron); const nextRun = interval.next().toDate(); const delay = nextRun.getTime() - Date.now(); const timeout = setTimeout(async () => { await this.executeCronTask(name, task); }, delay); this.cronJobs.set(name, timeout); } catch (error) { logger.error("Failed to setup cron job", { task: name, error }); } } /** * Get scheduler statistics */ getStats() { return { connected: this.connected, adapter: this.options.adapter, totalTasks: this.tasks.size, cronJobs: this.cronJobs.size, queues: this.queues.size, enabledTasks: Array.from(this.tasks.values()).filter((t) => t.enabled) .length, }; } /** * Generate unique job ID */ generateJobId() { return `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Shutdown task scheduler */ async shutdown() { if (!this.connected) { return; } logger.info("Shutting down task scheduler"); // Clear all cron jobs for (const timeout of this.cronJobs.values()) { clearTimeout(timeout); } this.cronJobs.clear(); // Disconnect adapter await this.adapter.disconnect(); this.connected = false; this.emit("shutdown"); logger.info("Task scheduler shutdown complete"); } /** * Check if scheduler is connected */ isConnected() { return this.connected && this.adapter.isConnected(); } } /** * Memory adapter for local task processing */ export class MemoryTaskAdapter extends TaskAdapter { jobs = new Map(); processors = new Map(); paused = new Set(); connected = false; async connect() { this.connected = true; } async disconnect() { this.jobs.clear(); this.processors.clear(); this.paused.clear(); this.connected = false; } async scheduleJob(job) { this.jobs.set(job.id, job); // Process immediately if not paused if (!this.paused.has(job.name)) { setTimeout(() => this.processJob(job), job.options.delay || 0); } } async processJob(job) { const processor = this.processors.get(job.name); if (!processor) { logger.warn("No processor for queue", { queue: job.name }); return; } try { job.processedOn = new Date(); await processor(job.data); job.finishedOn = new Date(); this.emit("jobCompleted", job); } catch (error) { job.failedReason = error instanceof Error ? error.message : String(error); job.attempts++; if (job.attempts < (job.options.retries || 0)) { // Retry job setTimeout(() => this.processJob(job), 1000 * job.attempts); } else { this.emit("jobFailed", job); } } } async processQueue(queueName, handler) { this.processors.set(queueName, handler); } async getQueueInfo(queueName) { const queueJobs = Array.from(this.jobs.values()).filter((j) => j.name === queueName); return { name: queueName, waiting: queueJobs.filter((j) => !j.processedOn).length, active: queueJobs.filter((j) => j.processedOn && !j.finishedOn).length, completed: queueJobs.filter((j) => j.finishedOn).length, failed: queueJobs.filter((j) => j.failedReason).length, paused: this.paused.has(queueName), }; } async removeJob(jobId) { return this.jobs.delete(jobId); } async retryJob(jobId) { const job = this.jobs.get(jobId); if (job && job.failedReason) { job.failedReason = undefined; job.attempts = 0; setTimeout(() => this.processJob(job), 0); return true; } return false; } async pauseQueue(queueName) { this.paused.add(queueName); } async resumeQueue(queueName) { this.paused.delete(queueName); // Process pending jobs const pendingJobs = Array.from(this.jobs.values()).filter((j) => j.name === queueName && !j.processedOn); for (const job of pendingJobs) { setTimeout(() => this.processJob(job), 0); } } isConnected() { return this.connected; } } /** * BullMQ adapter (placeholder - requires bullmq package) */ export class BullMQAdapter extends TaskAdapter { client; queues = new Map(); workers = new Map(); options; connected = false; constructor(options) { super(); this.options = options; } async connect() { try { // Try to load BullMQ dynamically try { const bullmqModule = await this.dynamicImportBullMQ(); if (bullmqModule) { const { Queue, Worker } = bullmqModule; // Initialize Redis connection for BullMQ const IORedis = await this.dynamicImportIORedis(); if (IORedis) { this.client = new IORedis.default(this.options.redis); } logger.info("BullMQ adapter connected", { redis: this.options.redis?.host || "localhost", }); } else { throw new Error("BullMQ not available"); } } catch (importError) { // Fallback to mock implementation for development logger.warn("BullMQ package not installed, using mock implementation"); this.createMockBullMQ(); } this.connected = true; } catch (error) { logger.error("Failed to connect to BullMQ", { error }); throw error; } } async disconnect() { try { // Cleanup queues and workers for (const [name, queue] of this.queues.entries()) { if (queue.close) { await queue.close(); } } for (const [name, worker] of this.workers.entries()) { if (worker.close) { await worker.close(); } } if (this.client && this.client.disconnect) { await this.client.disconnect(); } this.queues.clear(); this.workers.clear(); this.connected = false; logger.info("BullMQ adapter disconnected"); } catch (error) { logger.warn("Error disconnecting from BullMQ", { error }); this.connected = false; } } async scheduleJob(job) { if (!this.connected) { throw new Error("BullMQ not connected"); } try { // Use job name as queue name for BullMQ const queueName = job.name; let queue = this.queues.get(queueName); if (!queue) { if (this.client) { // Real BullMQ implementation const { Queue } = await this.dynamicImportBullMQ(); queue = new Queue(queueName, { connection: this.client }); this.queues.set(queueName, queue); } else { // Mock implementation queue = { add: async () => ({ id: job.id }) }; this.queues.set(queueName, queue); } } const jobOptions = { delay: job.options.delay, attempts: job.options.retries || 3, backoff: "exponential", }; await queue.add(job.name, job.data, jobOptions); logger.debug("BullMQ job scheduled", { jobId: job.id, queue: queueName, name: job.name, }); } catch (error) { logger.error("Failed to schedule BullMQ job", { job, error }); throw error; } } async processQueue(queueName, handler) { if (!this.connected) { throw new Error("BullMQ not connected"); } try { let worker = this.workers.get(queueName); if (!worker) { if (this.client) { // Real BullMQ implementation const { Worker } = await this.dynamicImportBullMQ(); worker = new Worker(queueName, async (job) => { const queueJob = { id: job.id, name: job.name, data: job.data, options: { retries: job.opts?.attempts || 3, timeout: job.opts?.timeout, }, attempts: job.attemptsMade || 0, processedOn: job.processedOn ? new Date(job.processedOn) : undefined, finishedOn: job.finishedOn ? new Date(job.finishedOn) : undefined, failedReason: job.failedReason, }; return await handler(queueJob); }, { connection: this.client }); this.workers.set(queueName, worker); } else { // Mock implementation worker = { process: async () => { } }; this.workers.set(queueName, worker); } } logger.debug("BullMQ queue processor started", { queue: queueName }); } catch (error) { logger.error("Failed to process BullMQ queue", { queueName, error }); throw error; } } async getQueueInfo(queueName) { try { const queue = this.queues.get(queueName); if (queue && queue.getWaiting) { // Real BullMQ implementation const [waiting, active, completed, failed] = await Promise.all([ queue.getWaiting(), queue.getActive(), queue.getCompleted(), queue.getFailed(), ]); return { name: queueName, waiting: waiting.length, active: active.length, completed: completed.length, failed: failed.length, }; } else { // Mock implementation return { name: queueName, waiting: 0, active: 0, completed: 0, failed: 0, }; } } catch (error) { logger.error("Failed to get BullMQ queue info", { queueName, error }); return { name: queueName, waiting: 0, active: 0, completed: 0, failed: 0, }; } } async removeJob(jobId) { try { // Find job across all queues for (const [queueName, queue] of this.queues.entries()) { if (queue.getJob) { const job = await queue.getJob(jobId); if (job && job.remove) { await job.remove(); logger.debug("BullMQ job removed", { jobId, queue: queueName }); return true; } } } logger.debug("BullMQ job not found for removal", { jobId }); return false; } catch (error) { logger.error("Failed to remove BullMQ job", { jobId, error }); return false; } } async retryJob(jobId) { try { // Find job across all queues for (const [queueName, queue] of this.queues.entries()) { if (queue.getJob) { const job = await queue.getJob(jobId); if (job && job.retry) { await job.retry(); logger.debug("BullMQ job retried", { jobId, queue: queueName }); return true; } } } logger.debug("BullMQ job not found for retry", { jobId }); return false; } catch (error) { logger.error("Failed to retry BullMQ job", { jobId, error }); return false; } } async pauseQueue(queueName) { try { const queue = this.queues.get(queueName); if (queue && queue.pause) { await queue.pause(); logger.debug("BullMQ queue paused", { queue: queueName }); } else { logger.debug("BullMQ queue pause (mock)", { queue: queueName }); } } catch (error) { logger.error("Failed to pause BullMQ queue", { queueName, error }); throw error; } } async resumeQueue(queueName) { try { const queue = this.queues.get(queueName); if (queue && queue.resume) { await queue.resume(); logger.debug("BullMQ queue resumed", { queue: queueName }); } else { logger.debug("BullMQ queue resume (mock)", { queue: queueName }); } } catch (error) { logger.error("Failed to resume BullMQ queue", { queueName, error }); throw error; } } async dynamicImportBullMQ() { try { return await new Function('return import("bullmq")')(); } catch { return null; } } async dynamicImportIORedis() { try { return await new Function('return import("ioredis")')(); } catch { return null; } } createMockBullMQ() { const mockJobs = new Map(); // Mock implementation for development logger.info("Using mock BullMQ implementation"); } isConnected() { return this.connected; } } /** * PgBoss adapter (placeholder - requires pg-boss package) */ export class PgBossAdapter extends TaskAdapter { boss; options; connected = false; constructor(options) { super(); this.options = options; } async connect() { try { // Try to load PgBoss dynamically try { const pgBossModule = await this.dynamicImportPgBoss(); if (pgBossModule) { const PgBoss = pgBossModule.default || pgBossModule; this.boss = new PgBoss(this.options.connectionString); await this.boss.start(); logger.info("PgBoss adapter connected", { database: this.options.connectionString.split("@")[1]?.split("/")[1] || "unknown", }); } else { throw new Error("pg-boss not available"); } } catch (importError) { // Fallback to mock implementation for development logger.warn("pg-boss package not installed, using mock implementation"); this.createMockPgBoss(); } this.connected = true; } catch (error) { logger.error("Failed to connect to PgBoss", { error }); throw error; } } async disconnect() { try { if (this.boss && this.boss.stop) { await this.boss.stop(); } this.connected = false; logger.info("PgBoss adapter disconnected"); } catch (error) { logger.warn("Error disconnecting from PgBoss", { error }); this.connected = false; } } async scheduleJob(job) { if (!this.connected) { throw new Error("PgBoss not connected"); } try { const jobOptions = { priority: job.options.priority || 0, retryLimit: job.options.retries || 3, retryDelay: 30, // seconds expireInMinutes: job.options.timeout ? Math.ceil(job.options.timeout / 60000) : 60, }; if (job.options.delay) { jobOptions.startAfter = new Date(Date.now() + job.options.delay); } if (this.boss && this.boss.send) { await this.boss.send(job.name, job.data, jobOptions); logger.debug("PgBoss job scheduled", { jobId: job.id, queue: job.name, priority: jobOptions.priority, }); } else { // Mock implementation logger.debug("PgBoss schedule job (mock)", { jobId: job.id }); } } catch (error) { logger.error("Failed to schedule PgBoss job", { job, error }); throw error; } } async processQueue(queueName, handler) { if (!this.connected) { throw new Error("PgBoss not connected"); } try { if (this.boss && this.boss.work) { await this.boss.work(queueName, async (job) => { const queueJob = { id: job.id, name: job.name, data: job.data, options: { retries: job.retryLimit || 3, timeout: job.expireInMinutes ? job.expireInMinutes * 60000 : undefined, }, attempts: job.retryCount || 0, processedOn: job.startedOn ? new Date(job.startedOn) : undefined, finishedOn: job.completedOn ? new Date(job.completedOn) : undefined, failedReason: job.output?.error, }; try { const result = await handler(queueJob); return result; } catch (error) { logger.error("PgBoss job processing failed", { jobId: job.id, queue: queueName, error, }); throw error; } }); logger.debug("PgBoss queue processor started", { queue: queueName }); } else { // Mock implementation logger.debug("PgBoss process queue (mock)", { queue: queueName }); } } catch (error) { logger.error("Failed to process PgBoss queue", { queueName, error }); throw error; } } async getQueueInfo(queueName) { try { if (this.boss && this.boss.getQueueSize) { const queueSize = await this.boss.getQueueSize(queueName); return { name: queueName, waiting: queueSize, active: 0, // PgBoss doesn't provide this directly completed: 0, // Would need custom query failed: 0, // Would need custom query }; } else { // Mock implementation return { name: queueName, waiting: 0, active: 0, completed: 0, failed: 0, }; } } catch (error) { logger.error("Failed to get PgBoss queue info", { queueName, error }); return { name: queueName, waiting: 0, active: 0, completed: 0, failed: 0, }; } } async removeJob(jobId) { try { if (this.boss && this.boss.cancel) { await this.boss.cancel(jobId); logger.debug("PgBoss job removed", { jobId }); return true; } else { logger.debug("PgBoss job removal (mock)", { jobId }); return false; } } catch (error) { logger.error("Failed to remove PgBoss job", { jobId, error }); return false; } } async retryJob(jobId) { try { if (this.boss && this.boss.retry) { await this.boss.retry(jobId); logger.debug("PgBoss job retried", { jobId }); return true; } else { logger.debug("PgBoss job retry (mock)", { jobId }); return false; } } catch (error) { logger.error("Failed to retry PgBoss job", { jobId, error }); return false; } } async pauseQueue(queueName) { try { // PgBoss doesn't have direct pause/resume, but we can stop workers logger.debug("PgBoss queue pause (mock)", { queue: queueName }); } catch (error) { logger.error("Failed to pause PgBoss queue", { queueName, error }); throw error; } } async resumeQueue(queueName) { try { // PgBoss doesn't have direct pause/resume, but we can restart workers logger.debug("PgBoss queue resume (mock)", { queue: queueName }); } catch (error) { logger.error("Failed to resume PgBoss queue", { queueName, error }); throw error; } } async dynamicImportPgBoss() { try { return await new Function('return import("pg-boss")')(); } catch { return null; } } createMockPgBoss() { const mockJobs = new Map(); this.boss = { start: async () => { }, stop: async () => { }, send: async (queue, data, options) => { const jobId = `pgboss_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; mockJobs.set(jobId, { queue, data, options }); return jobId; }, work: async (queue, handler) => { // Mock worker - in real implementation would poll for jobs }, getQueueSize: async (queue) => 0, cancel: async (jobId) => mockJobs.delete(jobId), retry: async (jobId) => mockJobs.has(jobId), }; logger.info("Using mock PgBoss implementation"); } isConnected() { return this.connected; } } /** * Task decorator for cron jobs */ export function Task(cron, options = {}) { return function (target, propertyKey, descriptor) { const metadata = { name: `${target.constructor.name}.${propertyKey}`, cron, ...options, }; // Store task metadata Reflect.defineMetadata("task:metadata", metadata, target, propertyKey); return descriptor; }; } /** * Queue decorator for queue jobs */ export function Queue(queueName, options = {}) { return function (target, propertyKey, descriptor) { const metadata = { name: `${target.constructor.name}.${propertyKey}`, queue: queueName, ...options, }; // Store task metadata Reflect.defineMetadata("task:metadata", metadata, target, propertyKey); return descriptor; }; } export default TaskScheduler; //# sourceMappingURL=task-scheduler.js.map