UNPKG

kasflowsjs

Version:

A lightweight communication system for client-server applications, especially designed for Roblox. Documentation available in English and Russian.

292 lines (247 loc) 9.33 kB
const express = require('express'); const cors = require('cors'); const fetch = require('node-fetch'); const logger = require('./logger'); class KasflowsBase { constructor() { this.eventsCallbacks = {}; this.messageforclient = {}; } on(event, callback) { this.eventsCallbacks[event] = callback; logger.info(`Callback added for event '${event}'`); } off(event) { delete this.eventsCallbacks[event]; logger.info(`Callback removed for event '${event}'`); } emit(event, data) { if (this.eventsCallbacks[event]) { this.eventsCallbacks[event](data); logger.info(`Event '${event}' emitted with data:`, data); } else { logger.warn(`No handler for event '${event}', ignoring`); } } } class Client extends KasflowsBase { constructor(url) { super(); this.url = url; this.name = null; this.connected = false; this.pingInterval = null; } async connect(name) { this.name = name; const response = await fetch(`${this.url}/statusws`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); const data = await response.json(); if (data.status === 'connected' || data.status === 'already connected') { this.connected = true; this.token = data.token; this.startPing(); } return data; } async disconnect() { if (!this.connected) { return { status: 'not connected' }; } if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } const response = await fetch(`${this.url}/disconnect`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: this.name, token: this.token }) }); const data = await response.json(); if (data.status === 'disconnected') { this.connected = false; this.name = null; this.token = null; } return data; } startPing() { this.pingInterval = setInterval(() => { if (this.connected) { fetch(`${this.url}/statusws`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: this.name }) }) .then(response => response.json()) .catch(err => { logger.error(`Ping error: ${err.message}`); }); } }, 5000); } async emit(event, data) { if (!this.connected) { return { status: 'not connected' }; } const dataWithToken = { ...data, token: this.token }; const response = await fetch(`${this.url}/sendmessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event, data: dataWithToken }) }); return response.json(); } async checkMessages() { if (!this.connected) { return { status: 'not connected' }; } const response = await fetch(`${this.url}/getmessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: this.name, token: this.token }) }); return response.json(); } } class Server { constructor(host = '127.0.0.1', port = 8000) { this.app = express(); this.host = host; this.port = port; this.connections = {}; this.kasflows = new KasflowsBase(); this.setupMiddleware(); this.setupRoutes(); this.startDisconnectChecker(); } setupMiddleware() { this.app.use(cors()); this.app.use(express.json()); } setupRoutes() { this.app.get('/', (req, res) => { res.json({ name: 'Kasflows', version: VERSION, api: { '/statusws': 'POST - Подключение клиента или обновление статуса', '/disconnect': 'POST - Отключение клиента', '/getmessage': 'POST - Получение сообщений для клиента', '/sendmessage': 'POST - Отправка сообщения на сервер', '/sendmessagetoclient': 'POST - Отправка сообщения конкретному клиенту', '/getclients': 'GET - Получение списка подключенных клиентов' } }); }); this.app.post('/statusws', (req, res) => { const { name } = req.body; if (name in this.connections) { this.connections[name].time = new Date(); logger.info(`Client ${name} pinged`); res.json({ status: 'already connected' }); } else { this.connections[name] = { time: new Date() }; this.kasflows.emit('connect', req.body); logger.info(`Client ${name} connected`); res.json({ status: 'connected' }); } }); this.app.post('/disconnect', (req, res) => { const { name } = req.body; if (name in this.connections) { delete this.connections[name]; this.kasflows.emit('disconnect', req.body); logger.info(`Client ${name} disconnected`); res.json({ status: 'disconnected' }); } else { res.json({ status: 'not connected' }); } }); this.app.post('/getmessage', (req, res) => { const { name } = req.body; if (name in this.kasflows.messageforclient) { const message = this.kasflows.messageforclient[name]; delete this.kasflows.messageforclient[name]; logger.info(`Message delivered to client ${name}`); res.json({ status: 'success', message }); } else { res.json({ status: 'no message' }); } }); this.app.post('/sendmessage', (req, res) => { const { event, data } = req.body; try { if (data.sender && data.sender in this.connections) { this.kasflows.emit(event, data); res.json({ status: 'success' }); } else { res.status(400).json({ status: 'error', message: 'Client not found' }); } } catch (error) { logger.error(`Error emitting event ${event}: ${error.message}`); res.status(400).json({ status: 'error', message: error.message }); } }); this.app.post('/sendmessagetoclient', (req, res) => { const { name, message } = req.body; this.kasflows.messageforclient[name] = message; logger.info(`Message queued for client ${name}`); res.json({ status: 'success' }); }); this.app.get('/getclients', (req, res) => { res.json({ clients: Object.keys(this.connections) }); }); } startDisconnectChecker() { setInterval(() => { const currentTime = new Date(); const toDisconnect = []; for (const [name, info] of Object.entries(this.connections)) { const lastTime = info.time; if ((currentTime - lastTime) / 1000 > 10) { toDisconnect.push(name); } } toDisconnect.forEach(name => { delete this.connections[name]; logger.info(`Client ${name} disconnected due to timeout`); this.kasflows.emit('disconnect', { name }); }); }, 5000); } start() { return new Promise((resolve) => { const server = this.app.listen(this.port, this.host, () => { logger.info(`Server running at http://${this.host}:${this.port}`); resolve(server); }); }); } } const VERSION = '1.0.0'; const Kasflows = new KasflowsBase(); module.exports = { KasflowsBase, Client, Server, Kasflows, VERSION, logger };