UNPKG

@uisap/core

Version:

A modular Fastify-based framework inspired by Laravel

240 lines (217 loc) 9.49 kB
// uisap-core/src/worker.js (NİHAİ DÜZELTİLMİŞ HALİ) import { Application, Config, Logger, KeyValueFacade, EventFacade, DatabaseProvider, QueueProvider, BroadcastProvider, RouteProvider, ServiceProvider, Event, } from "./src/index.js"; import { Server } from "socket.io"; import { createServer } from "http"; import { pathToFileURL } from "url"; import dotenv from "dotenv"; import { join, dirname } from "path"; import { readdir } from "fs/promises"; import { program } from "commander"; import Redis from "ioredis"; // Komut satırı argümanları program .option( "--queue <queues>", "İşlenecek kuyrukların virgülle ayrılmış listesi", "default,flows,broadcast" ) .option("--tries <tries>", "Yeniden deneme sayısı", "3") .parse(process.argv); const options = program.opts(); const queuesToProcess = options.queue.split(",").map((q) => q.trim()); // Worker'ın kendi içinde kullanacağı genel bir olay sınıfı class JobFailed extends Event { constructor(data) { super(data); } } (async () => { dotenv.config({ path: join(process.cwd(), ".env") }); const config = await Config.load(); const app = new Application({ ...config.app, database: config.database, cors: config.cors, queue: config.queue, schedule: config.schedule, etl: config.etl, routes: config.routes, }); const logger = app.fastify.logger; logger.info("Custom Worker Process Starting..."); try { // Projeye özel provider'ları otomatik olarak yükle const providersDir = join(process.cwd(), 'app', 'providers'); try { const files = await readdir(providersDir); for (const file of files.filter(f => f.endsWith('.js'))) { const filePath = pathToFileURL(join(providersDir, file)).href; const module = await import(filePath); const ProviderClass = Object.values(module).find( (p) => p && p.prototype instanceof ServiceProvider ); if (ProviderClass) { app.provider(ProviderClass); logger.debug(`Worker auto-loaded provider: ${ProviderClass.name}`); } } } catch (err) { if (err.code !== 'ENOENT') { logger.error(`Could not auto-load providers from ${providersDir}`, { error: err.message }); } } // Çekirdek provider'ları kaydet app.provider(DatabaseProvider); app.provider(QueueProvider); app.provider(BroadcastProvider); app.provider(RouteProvider); // Uygulamayı ve provider'ları başlat await app.bootstrap({ isCli: true }); logger.info("Worker: Application bootstrapped. Container is ready."); // Facade'leri başlat EventFacade.setFastify(app.fastify); KeyValueFacade.setFastify(app.fastify); // Socket.IO Sunucusunu başlat const httpServer = createServer(); const io = new Server(httpServer, { cors: { origin: "*" } }); app.fastify.io = io; httpServer.listen(config.app?.workerPort || 4116, config.app?.workerHost || "0.0.0.0", () => { logger.info("Worker Socket.IO server started", { port: config.app?.workerPort || 4116, host: config.app?.workerHost || "0.0.0.0" }); }); // Redis subscriber'ı başlat const redisConfig = config.database?.connections?.redis?.connection || {}; const subscriber = new Redis(redisConfig); const webhookTestChannel = 'webhook-test-events'; subscriber.subscribe(webhookTestChannel, (err, count) => { if (err) { logger.error('Failed to subscribe to Redis channel', { channel: webhookTestChannel, error: err.message }); return; } logger.info(`Worker subscribed to Redis channel: ${webhookTestChannel}. Count: ${count}`); }); subscriber.on('message', (channel, message) => { if (channel === webhookTestChannel) { try { const payload = JSON.parse(message); const { targetSessionId, data } = payload; if (targetSessionId) { const socketIORoom = `webhook-test-${targetSessionId}`; io.to(socketIORoom).emit('webhook:test:received', data); logger.info(`Forwarded webhook test data from Redis to Socket.IO room`, { room: socketIORoom }); } } catch(e) { logger.error('Failed to parse and forward message from Redis', { error: e.message, message }); } } }); // Socket.IO bağlantı olaylarını dinle io.on("connection", (socket) => { logger.info("Worker: New client connected", { id: socket.id }); socket.on("joinRoom", async (channel) => { socket.join(channel); logger.info("Worker: Client joined channel", { channel, id: socket.id }); }); socket.on("disconnect", (reason) => { logger.info("Worker: Client disconnected", { id: socket.id, reason }); }); }); // ---------- DÜZELTİLMİŞ BLOK BAŞLANGICI ---------- // Kuyruk İşleyicilerini (Job Handlers) Yükle const jobHandlers = { // broadcastMessage gibi framework içi işleyicileri burada tanımlayın broadcastMessage: async (data) => { const retryConfig = config.broadcast?.retryOptions || { enabled: true, maxAttempts: 5, delay: 5000 }; const { channel, event, message, attempts = 0 } = data; const jobLogger = logger.child({ job: 'broadcastMessage', channel, event }); try { const clientsInRoom = (await io.in(channel).allSockets()).size; jobLogger.debug(`Broadcast mesajı işleniyor. Kanaldaki istemci sayısı: ${clientsInRoom}`); if (clientsInRoom > 0) { io.to(channel).emit(event, message); jobLogger.info("Broadcast mesajı gönderildi", { payload: message }); } else { if (retryConfig.enabled && attempts < retryConfig.maxAttempts) { jobLogger.warn(`Kanalda istemci yok. Mesaj ${retryConfig.delay}ms sonra yeniden denenecek. Deneme: ${attempts + 1}/${retryConfig.maxAttempts}`); setTimeout(() => { const queue = app.container.make("queue"); queue.addTo("broadcast", "broadcastMessage", { ...data, attempts: attempts + 1 }); }, retryConfig.delay); } else { jobLogger.error(`Broadcast mesajı gönderilemedi. Maksimum deneme sayısına ulaşıldı (${retryConfig.maxAttempts}) veya yeniden deneme kapalı.`); } } } catch (err) { jobLogger.error("Broadcast mesajı işlenirken kritik hata", { error: err.message, stack: err.stack }); throw err; } }, }; // Projeye özel handler'ları config'den yükle ve jobHandlers objesine ekle const customHandlers = config.queue?.handlers || {}; for (const [taskType, handlerPath] of Object.entries(customHandlers)) { try { const handlerUrl = pathToFileURL(handlerPath).href; const handlerModule = await import(handlerUrl); const HandlerClass = handlerModule.default || handlerModule; if (typeof HandlerClass !== "function") { throw new Error(`Handler ${taskType} is not a constructor: ${typeof HandlerClass}`); } // Sınıfı çağıran bir fonksiyonu kaydet jobHandlers[taskType] = async (data) => { const instance = new HandlerClass(data); await instance.handle(app); }; logger.debug(`Dynamic job handler loaded: ${taskType}`); } catch (err) { logger.error(`Failed to load handler for job type: ${taskType}`, { error: err.message }); } } // Kuyrukları İşle const queueInstance = app.container.make("queue"); queuesToProcess.forEach((queueName) => { const queue = queueInstance.queues.get(queueName); if (queue) { // '*' ile bu kuyruktaki tüm işleri yakala queue.process('*', 5, async (job) => { const { taskType, data } = job.data; const jobLogger = logger.child({ queue: queueName, job: taskType, jobId: job.id }); jobLogger.info(`Processing job...`); try { const handlerFunction = jobHandlers[taskType]; // Artık bu bir fonksiyon, sınıf değil if (!handlerFunction) { throw new Error(`No handler registered for task type: ${taskType}`); } await handlerFunction(data); // Doğrudan fonksiyonu çağır } catch (error) { jobLogger.error(`Job processing failed.`, { error: error.message, stack: error.stack }); throw error; } }); queue.on('failed', async (job, err) => { const jobLogger = logger.child({ job: 'failedJobListener', jobId: job.id, taskType: job.data.taskType }); jobLogger.error(`Job failed permanently: ${job.data.taskType}`, { error: err.message, attemptsMade: job.attemptsMade }); await EventFacade.fire(new JobFailed({ job: job.data, error: err })); }); logger.info(`Worker is now processing queue: '${queueName}'`); } else { logger.error(`Queue '${queueName}' not found.`); } }); // ---------- DÜZELTİLMİŞ BLOK SONU ---------- } catch (err) { console.error("CRITICAL: Worker process failed to start.", { error: err.message, stack: err.stack }); process.exit(1); } })();