@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
JavaScript
"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;