UNPKG

@dwwoelfel/lds

Version:

Logical decoding server for PostgreSQL, monitors for new/edited/deleted rows and announces them to interested clients.

152 lines 5.83 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const index_1 = require("./index"); const WebSocket = require("ws"); const CONNECTION_STRING = process.env.LD_DATABASE_URL; const TABLE_PATTERN = process.env.LD_TABLE_PATTERN || "*.*"; const SLOT_NAME = process.env.LD_SLOT_NAME || "postgraphile"; const PORT = parseInt(process.env.LD_PORT || process.env.PORT || "", 10) || 9876; const HOST = process.env.LD_HOST || "127.0.0.1"; // Maximum number of expected clients const SLOTS = parseInt(process.env.LD_MAX_CLIENTS || "", 10) || 50; const SLEEP_DURATION = Math.max(1, parseInt(process.env.LD_WAIT || "", 10) || 125); const stringify = JSON.stringify; async function main() { if (!CONNECTION_STRING) { throw new Error("No connection string provided, please set envvar LD_DATABASE_URL."); } // Now slot is created, create websocket server const wss = new WebSocket.Server({ port: PORT, host: HOST }); const clients = []; const channels = {}; // Send keepalive every 25 seconds setInterval(() => { clients.forEach(ws => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ _: "KA", })); } }); }, 25000); wss.on("connection", function connection(ws) { clients.push(ws); ws.on("close", () => { const i = clients.indexOf(ws); if (i >= 0) { clients.splice(i, 1); } // Release all the subscriptions // TODO: do this more performantly!! for (const schema of Object.keys(channels)) { for (const table of Object.keys(channels[schema])) { for (const stringifiedKey of Object.keys(channels[schema][table])) { const channelClients = channels[schema][table][stringifiedKey]; const index = channelClients.indexOf(ws); if (index >= 0) { channelClients[index] = null; } } } } }); ws.on("message", function incoming(rawMessage) { const message = rawMessage.toString("utf8"); let topicJSON; let sub; if (message.startsWith("SUB ")) { topicJSON = message.substr(4); sub = true; } else if (message.startsWith("UNSUB ")) { topicJSON = message.substr(6); sub = false; } else { console.error("Unknown command", message); return; } const topic = JSON.parse(topicJSON); if (!Array.isArray(topic)) return console.error("Not an array"); if (topic.length < 2) return console.error("Too short"); if (topic.length > 3) return console.error("Too long"); const [schema, table, key] = topic; if (typeof schema !== "string") { return console.error("Schema not a string"); } if (typeof table !== "string") return console.error("Table not a string"); if (key && !Array.isArray(key)) { return console.error("Invalid key spec", topic); } const stringifiedKey = key ? stringify(key) : ""; if (!channels[schema]) { channels[schema] = {}; } if (!channels[schema][table]) { channels[schema][table] = {}; } if (!channels[schema][table][stringifiedKey]) { channels[schema][table][stringifiedKey] = new Array(SLOTS); } const channelClients = channels[schema][table][stringifiedKey]; const i = channelClients.indexOf(ws); if (sub) { if (i >= 0) { console.error("Socket is already registered for ", stringifiedKey); return; } const emptyIndex = channelClients.findIndex(s => !s); if (emptyIndex < 0) { console.error("All sockets are full"); return; } channelClients[emptyIndex] = ws; } else { if (i < 0) { console.error("Socket is not registered for ", stringifiedKey); return; } channelClients[i] = null; } }); ws.send(JSON.stringify({ _: "ACK", })); }); const callback = function (announcement) { const { _: kind, schema, table } = announcement; if (!channels[schema] || !channels[schema][table]) return; const stringifiedKey = announcement._ !== "insertC" && announcement._ !== "updateC" ? stringify(announcement.keys) : ""; const channelClients = channels[schema][table][stringifiedKey]; if (!channelClients) return; const msg = JSON.stringify(announcement); for (const socket of channelClients) { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(msg); } } if (kind === "delete") { delete channels[schema][table][stringifiedKey]; } }; await index_1.default(CONNECTION_STRING, callback, { slotName: SLOT_NAME, tablePattern: TABLE_PATTERN, sleepDuration: SLEEP_DURATION, }); } main().catch(e => { console.error(e); process.exit(1); }); //# sourceMappingURL=cli.js.map