UNPKG

@socket.io/postgres-adapter

Version:

The Socket.IO Postgres adapter, allowing to broadcast events between several Socket.IO servers

160 lines (159 loc) 6.16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PubSubClient = exports.randomId = exports.hasBinary = void 0; const node_crypto_1 = require("node:crypto"); const debug_1 = require("debug"); const socket_io_adapter_1 = require("socket.io-adapter"); const msgpack_1 = require("@msgpack/msgpack"); const debug = (0, debug_1.default)("socket.io-postgres-adapter"); function hasBinary(obj, toJSON) { if (!obj || typeof obj !== "object") { return false; } if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) { return true; } if (Array.isArray(obj)) { for (let i = 0, l = obj.length; i < l; i++) { if (hasBinary(obj[i])) { return true; } } return false; } for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) { return true; } } if (obj.toJSON && typeof obj.toJSON === "function" && !toJSON) { return hasBinary(obj.toJSON(), true); } return false; } exports.hasBinary = hasBinary; function randomId() { return (0, node_crypto_1.randomBytes)(8).toString("hex"); } exports.randomId = randomId; class PubSubClient { constructor(pool, opts, isFromSelf, onMessage) { this.pool = pool; this.opts = opts; this.isFromSelf = isFromSelf; this.onMessage = onMessage; this.channels = new Set(); this.initClient().then(() => { }); // ignore error this.cleanupTimer = setInterval(async () => { try { debug("removing old events"); await pool.query(`DELETE FROM ${opts.tableName} WHERE created_at < now() - interval '${opts.cleanupInterval} milliseconds'`); } catch (err) { opts.errorHandler(err); } }, opts.cleanupInterval); } scheduleReconnection() { const reconnectionDelay = Math.floor(2000 * (0.5 + Math.random())); debug("reconnection in %d ms", reconnectionDelay); this.reconnectTimer = setTimeout(() => this.initClient(), reconnectionDelay); } async initClient() { try { debug("acquiring client from the pool"); const client = await this.pool.connect(); debug("client acquired"); client.on("notification", async (msg) => { try { let message = JSON.parse(msg.payload); if (this.isFromSelf(message)) { return; } if (message.attachmentId) { const result = await this.pool.query(`SELECT payload FROM ${this.opts.tableName} WHERE id = $1`, [message.attachmentId]); const fullMessage = (0, msgpack_1.decode)(result.rows[0].payload); this.onMessage(fullMessage); } else { this.onMessage(message); } } catch (e) { this.opts.errorHandler(e); } }); client.on("end", () => { debug("client was closed, scheduling reconnection..."); this.client = undefined; this.scheduleReconnection(); }); for (const channel of this.channels) { debug("client listening to %s", channel); await client.query(`LISTEN "${channel}"`); } this.client = client; } catch (e) { debug("error while initializing client, scheduling reconnection..."); this.scheduleReconnection(); } } addNamespace(namespace) { const channel = `${this.opts.channelPrefix}#${namespace}`; if (this.channels.has(channel)) { return; } this.channels.add(channel); if (this.client) { debug("client listening to %s", channel); this.client.query(`LISTEN "${channel}"`).catch(() => { }); } } async publish(message) { try { if ([ socket_io_adapter_1.MessageType.BROADCAST, socket_io_adapter_1.MessageType.BROADCAST_ACK, socket_io_adapter_1.MessageType.SERVER_SIDE_EMIT, socket_io_adapter_1.MessageType.SERVER_SIDE_EMIT_RESPONSE, ].includes(message.type) && hasBinary(message)) { return this.publishWithAttachment(message); } const payload = JSON.stringify(message); if (Buffer.byteLength(payload) > this.opts.payloadThreshold) { return this.publishWithAttachment(message); } const channel = `${this.opts.channelPrefix}#${message.nsp}`; debug("sending event of type %s to channel %s", message.type, channel); await this.pool.query("SELECT pg_notify($1, $2)", [channel, payload]); } catch (err) { this.opts.errorHandler(err); } } async publishWithAttachment(message) { const payload = (0, msgpack_1.encode)(message); const channel = `${this.opts.channelPrefix}#${message.nsp}`; debug("sending event of type %s with attachment to channel %s", message.type, channel); const result = await this.pool.query(`INSERT INTO ${this.opts.tableName} (payload) VALUES ($1) RETURNING id;`, [payload]); const attachmentId = result.rows[0].id; const headerPayload = JSON.stringify({ uid: message.uid, type: message.type, attachmentId, }); await this.pool.query("SELECT pg_notify($1, $2)", [channel, headerPayload]); } close() { if (this.client) { this.client.removeAllListeners("end"); this.client.release(); this.client = undefined; } clearTimeout(this.reconnectTimer); clearInterval(this.cleanupTimer); } } exports.PubSubClient = PubSubClient;