@graphile/pg-pubsub
Version:
Subscriptions plugin for PostGraphile using PostgreSQL's LISTEN/NOTIFY
177 lines • 8.27 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const graphql_subscriptions_1 = require("graphql-subscriptions");
const events_1 = require("events");
const debug_1 = require("debug");
const PgGenericSubscriptionPlugin_1 = require("./PgGenericSubscriptionPlugin");
const PgSubscriptionResolverPlugin_1 = require("./PgSubscriptionResolverPlugin");
const RECONNECT_BASE_DELAY = 100;
const RECONNECT_MAX_DELAY = 30000;
const noop = () => { };
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const debugSubscriptions = (0, debug_1.default)("postgraphile:subscriptions");
const plugin = {
["cli:flags:add:schema"](addFlag) {
addFlag("-S, --simple-subscriptions", "⚡️[experimental] add simple subscription support");
addFlag("--subscription-authorization-function [schemaDotFunctionName]", "⚡️[experimental] PG function to call to check user is allowed to subscribe");
return addFlag;
},
["cli:library:options"](options, { config, cliOptions }) {
const { simpleSubscriptions = false, subscriptionAuthorizationFunction = null, } = Object.assign(Object.assign({}, config.options), cliOptions);
return Object.assign(Object.assign({}, options), { simpleSubscriptions,
subscriptionAuthorizationFunction });
},
["postgraphile:options"](incomingOptions, { pgPool }) {
const eventEmitter = new events_1.EventEmitter();
if (incomingOptions.subscriptionEventEmitterMaxListeners != null) {
eventEmitter.setMaxListeners(incomingOptions.subscriptionEventEmitterMaxListeners);
}
const { simpleSubscriptions, subscriptionAuthorizationFunction } = incomingOptions;
const pubsub = new graphql_subscriptions_1.PubSub({
eventEmitter,
});
const handleNotification = function (msg) {
let payload;
if (msg.payload) {
try {
payload = JSON.parse(msg.payload);
}
catch (e) {
debugSubscriptions("Failed to parse payload JSON");
debugSubscriptions(e);
// ignore
}
}
pubsub.publish(msg.channel, payload);
};
let listeningClient;
const cleanClient = function (client) {
client.removeListener("error", noop);
client.removeListener("notification", handleNotification);
clearInterval(client["keepAliveInterval"]);
delete client["keepAliveInterval"];
if (client === listeningClient) {
listeningClient = null;
}
};
const releaseClient = function (client) {
if (!client) {
return;
}
if (client) {
cleanClient(client);
client.release();
}
};
const listenToChannelWithClient = async function (client, channel) {
const sql = "LISTEN " + client.escapeIdentifier(channel);
await client.query(sql);
};
const unlistenFromChannelWithClient = async function (client, channel) {
const sql = "UNLISTEN " + client.escapeIdentifier(channel);
await client.query(sql);
};
const channelListenCount = {};
const listen = async function (channel) {
channelListenCount[channel] = (channelListenCount[channel] || 0) + 1;
if (channelListenCount[channel] === 1 && listeningClient) {
await listenToChannelWithClient(listeningClient, channel);
}
};
const unlisten = async function (channel) {
channelListenCount[channel] = (channelListenCount[channel] || 1) - 1;
if (channelListenCount[channel] === 0 && listeningClient) {
await unlistenFromChannelWithClient(listeningClient, channel);
}
};
const aL = eventEmitter.addListener;
eventEmitter.addListener = function (name, hook) {
if (typeof name === "string") {
listen(name).catch(e => {
// eslint-disable-next-line no-console
console.error("Error occurred when unlistening:", e.message);
});
}
return aL.call(this, name, hook);
};
const rL = eventEmitter.removeListener;
eventEmitter.removeListener = function (name, hook) {
if (typeof name === "string") {
unlisten(name).catch(e => {
// eslint-disable-next-line no-console
console.error("Error occurred when unlistening:", e.message);
});
}
return rL.call(this, name, hook);
};
const setupClient = async function (attempts = 0) {
if (attempts > 0 && attempts % 5 === 0) {
// eslint-disable-next-line no-console
console.warn(`WARNING: @graphile/pg-pubsub cannot establish a connection to the server; reattempting with exponential backoff (attempt ${attempts})`);
}
// Permanently check client out of the pool
let client;
try {
client = await pgPool.connect();
client.on("error", noop);
}
catch (e) {
// Exponential back-off
const delay = Math.floor(Math.min(RECONNECT_MAX_DELAY, RECONNECT_BASE_DELAY * Math.random() * 2 ** attempts));
await sleep(delay);
return setupClient(attempts + 1);
}
listeningClient = client;
listeningClient.addListener("notification", handleNotification);
// Every 25 seconds, send 'select 1' to keep the connection alive
client["keepAliveInterval"] = setInterval(() => {
client.query("select 1").catch(e => {
// eslint-disable-next-line no-console
console.error("Listen client keepalive error (will attempt reconnect):");
// eslint-disable-next-line no-console
console.error(e);
releaseClient(client);
if (!pgPool.ending) {
setupClient();
}
});
}, 25000);
const channels = Object.entries(channelListenCount)
.filter(([_channel, count]) => count > 0)
.map(([channel]) => channel);
try {
await Promise.all(channels.map(channel => listenToChannelWithClient(client, channel)));
}
catch (e) {
// eslint-disable-next-line no-console
console.error(`Error occurred when listening to channel; retrying after ${attempts * 2} seconds`);
// eslint-disable-next-line no-console
console.error(e);
releaseClient(client);
if (!pgPool.ending) {
await sleep(attempts * 2000);
return setupClient(attempts + 1);
}
}
};
setupClient().catch(e => {
// eslint-disable-next-line no-console
console.error("Error occurred when trying to set up initial client. Current state is undefined. Suggest server restart.", e);
});
pgPool.on("remove", (client) => {
if (client === listeningClient) {
cleanClient(client);
if (!pgPool.ending) {
setupClient();
}
}
});
return Object.assign(Object.assign({}, incomingOptions), { graphileBuildOptions: Object.assign(Object.assign({}, incomingOptions.graphileBuildOptions), { pubsub, pgSubscriptionAuthorizationFunction: subscriptionAuthorizationFunction }), appendPlugins: [
...(incomingOptions.appendPlugins || []),
PgSubscriptionResolverPlugin_1.default,
...(simpleSubscriptions ? [PgGenericSubscriptionPlugin_1.default] : []),
] });
},
};
exports.default = plugin;
//# sourceMappingURL=index.js.map