var config = require('./config'); var set = require('simplesets').Set; var queue = require('qu'); var WebSocketServer = require('ws').Server; var wss_receiver = new WebSocketServer({host: config.get_host, port: config.get_port}); var wss_sender = new WebSocketServer({host: config.post_host, port: config.post_port}); var messages = new queue(); var followers = new set(); var pollers = new set(); var max_queue = config.max_queue || 50; var long_poll_timeout = config.long_poll_timeout || 60000; var message_id = Date.now(); if (typeof String.prototype.startsWith != 'function') { String.prototype.startsWith = function (str){ return this.slice(0, str.length) == str; }; } messages.catch_up = function (client) { this.each(function (message) { if (message.id > client.last_msg) client.got_message(message); }); }; messages.post = function (channel, message) { message = { id: ++message_id, channel: channel, message: message }; this.push(message); if (this.length > max_queue) this.shift(); followers.each(function (client) { client.got_message(message); }); pollers.each(function (request) { request.got_message(message); }); return message.id; }; messages.last = function () { return this.tail().id; }; wss_receiver.on('connection', function (socket) { socket.channel = null; socket.last_msg = 0; var commands = { start_msg: function (request) { socket.last_msg = request.start; }, set_filter: function (request) { var filter = {}; if (Array.isArray(request.filter) && request.filter.length > 0 && request.filter.every(function (channel, index, array) { if (typeof channel != 'string') return false; filter[channel] = true; return true; })) { socket.filter = filter; followers.add(socket); messages.catch_up(socket); } else { socket.send(JSON.stringify({ status: 'error', code: 'invalid-filter', message: 'invalid filter: ' + request.filter })); } }, }; socket.got_message = function (message) { if (message.channel in socket.filter) socket.send(JSON.stringify(message)); socket.last_msg = message.id; }; socket.on('message', function (request) { try { request = JSON.parse(request); } catch (err) { socket.send(JSON.stringify({ status: 'error', code: 'syntax-error', message: err.message })); return; } request.command = request.command.replace(/-/g, '_'); if (request.command in commands) commands[request.command](request); else socket.send(JSON.stringify({ status: 'error', code: 'bad-command', message: 'bad command: ' + request.command })); }); socket.on('close', function(code, message) { followers.remove(socket); }); }); wss_sender.on('connection', function (socket) { var commands = { post: function (request) { if (typeof request.channel != 'string') return { status: 'error', code: 'invalid-channel' }; return { status: 'success', id: messages.post(request.channel, request.message) }; }, last_msg: function (request) { return { status: 'success', id: message_id, }; } }; socket.on('message', function (request) { try { request = JSON.parse(request); } catch (err) { socket.send(JSON.stringify({ status: 'error', code: 'syntax-error', message: err.message })); return; } request.command = request.command.replace(/-/g, '_'); if (request.command in commands) socket.send(JSON.stringify(commands[request.command](request))); else socket.send(JSON.stringify({ status: 'error', code: 'bad-command', message: 'bad command: ' + request.command })); }); }); var url = require('url'); require('http').createServer(function (req, res) { var parts = url.parse(req.url, true); if (!parts.pathname.startsWith('/channels/')) { res.writeHead(404, {'Content-Type': 'text/plain'}); res.end('404 Not Found'); return; } var channels = parts.pathname.slice(10).split('|'); if (channels.length == 1 && !channels[0].length) { res.writeHead(400, {'Content-Type': 'text/plain'}); res.end('400 Bad Request'); return; } req.channels = {}; req.last_msg = parseInt(parts.query.last); if (isNaN(req.last_msg)) req.last_msg = 0; channels.forEach(function (channel) { req.channels[channel] = true; }); req.on('close', function () { pollers.remove(req); }); req.got_message = function (message) { if (message.channel in req.channels) { res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify(message)); pollers.remove(req); return true; } return false; }; var got = false; messages.each(function (message) { if (!got && message.id > req.last_msg) got = req.got_message(message); }); if (!got) { pollers.add(req); res.setTimeout(long_poll_timeout, function () { pollers.remove(req); res.writeHead(504, {'Content-Type': 'application/json'}); res.end('{"error": "timeout"}'); }); } }).listen(config.http_port, config.http_host);