@uisap/core
Version:
A modular Fastify-based framework inspired by Laravel
240 lines (217 loc) • 9.49 kB
JavaScript
// 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);
}
})();