UNPKG

@graphile/pg-pubsub

Version:

Subscriptions plugin for PostGraphile using PostgreSQL's LISTEN/NOTIFY

177 lines 8.27 kB
"use strict"; 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