UNPKG

@imqueue/pg-cache

Version:

PostgreSQL managed cache on Redis for @imqueue-based service methods

192 lines 7.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChannelOperation = void 0; exports.PgCache = PgCache; /*! * I'm Queue Software Project * Copyright (C) 2025 imqueue.com <support@imqueue.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * If you want to use this code in a closed source (commercial) project, you can * purchase a proprietary commercial license. Please contact us at * <support@imqueue.com> to get commercial licensing options. */ const rpc_1 = require("@imqueue/rpc"); const tag_cache_1 = require("@imqueue/tag-cache"); const pg_pubsub_1 = require("@imqueue/pg-pubsub"); const env_1 = require("./env"); const RX_TRIGGER = new RegExp('create\\s+(or\\s+replace)?function\\s+' + 'post_change_notify_trigger\\s+\\([^)]*\\).*?returns\\s+trigger', 'i'); /** * Checks if a given definition valid. If not - will return default trigger * definition. * * @see PG_CACHE_TRIGGER * @access private * @param {string} [definition] * @return {string} */ function triggerDef(definition) { if (!RX_TRIGGER.test(definition + '')) { return env_1.PG_CACHE_TRIGGER; } return definition; } /** * Installs database triggers * * @access private * @param {string[]} channels * @param {Client} pg * @param {string} triggerDefinition * @param {ILogger} logger * @return {Promise<void>} */ async function install(channels, pg, triggerDefinition, logger) { try { await pg.query(triggerDefinition); } catch (err) { env_1.PG_CACHE_DEBUG && logger.info('PgCache: create trigger function errored:', err); } await Promise.all(channels.map(async (channel) => { try { await pg.query(`CREATE TRIGGER "post_change_notify" AFTER INSERT OR UPDATE OR DELETE ON "${channel}" FOR EACH ROW EXECUTE PROCEDURE post_change_notify_trigger()`); env_1.PG_CACHE_DEBUG && logger.info(`PgCache: trigger created on ${channel}!`); } catch (err) { env_1.PG_CACHE_DEBUG && logger.info(`PgCache: create trigger on ${channel} errored:`, err); } })); } var ChannelOperation; (function (ChannelOperation) { // noinspection JSUnusedGlobalSymbols ChannelOperation["INSERT"] = "INSERT"; ChannelOperation["UPDATE"] = "UPDATE"; ChannelOperation["DELETE"] = "DELETE"; })(ChannelOperation || (exports.ChannelOperation = ChannelOperation = {})); function needInvalidate(payload, filter) { if (Array.isArray(filter)) { return !~filter.indexOf(payload.operation); } else if (typeof filter === 'function') { payload.timestamp = new Date(payload.timestamp); return !!filter(payload); } return true; } function publish(self, channel, payload, tag) { if (typeof self.publish !== 'function') { env_1.PG_CACHE_DEBUG && self.logger.info(`PgCache: publish method does not exist on ${self.constructor.name}`); return; } self.publish({ channel, payload, tag }) .then((result) => { env_1.PG_CACHE_DEBUG && self.logger.info(`PgCache: tag '${tag}' published to client with:`, channel); return result; }) .catch((err) => self.logger.warn(`PgCache: error publishing '${tag}':`, err)); } function invalidate(self, tag) { self.taggedCache.invalidate(tag) .then((result) => { env_1.PG_CACHE_DEBUG && self.logger.info(`PgCache: key '${tag}' invalidated!`); return result; }) .catch((err) => self.logger.warn(`PgCache: error invalidating '${tag}':`, err)); } // noinspection JSUnusedGlobalSymbols function PgCache(options) { return (constructor) => { const init = constructor.prototype.start; const pgCacheChannels = constructor.prototype.pgCacheChannels; class CachedService { taggedCache; pgCacheChannels; pubSub; async start(...args) { this.pubSub = new pg_pubsub_1.PgPubSub({ connectionString: options.postgres, }); if (init && typeof init === 'function') { await init.apply(this, args); } const logger = (this.logger || console); const prefix = options.prefix || constructor.name; let cache; if (options.redisCache) { cache = options.redisCache; } else if (options.redis) { cache = await new rpc_1.RedisCache() .init({ ...options.redis, prefix, logger }); } else if (this.cache) { cache = this.cache; } else { throw new TypeError('PgCache: either one of redisCache or ' + 'redisConnectionString option must be provided!'); } this.taggedCache = new tag_cache_1.TagCache(cache); const pgChannels = this.pgCacheChannels || pgCacheChannels || {}; const channels = Object.keys(pgChannels); if (!(channels && channels.length)) { return; } const className = constructor.name; const maxListeners = channels.length * 2; this.pubSub.channels.setMaxListeners(maxListeners); this.pubSub.setMaxListeners(maxListeners); this.pubSub.pgClient.setMaxListeners(maxListeners); for (const channel of channels) { this.pubSub.channels.on(channel, payload => { env_1.PG_CACHE_DEBUG && logger.info('PgCache: database event caught:', channel, payload); const methods = pgChannels[channel] || []; const data = payload; for (const [method, filter] of methods) { const useTag = (0, rpc_1.signature)(className, method, []); if (needInvalidate(data, filter)) { invalidate(this, useTag); options.publish !== false && publish(this, channel, payload, useTag); } } }); } this.pubSub.on('connect', async () => { await install(Object.keys(pgChannels), this.pubSub.pgClient, triggerDef(options.triggerDefinition), logger); env_1.PG_CACHE_DEBUG && logger.info(`PgCache: triggers installed for ${className}`); await Promise.all(channels.map(async (channel) => await this.pubSub.listen(channel))); env_1.PG_CACHE_DEBUG && logger.info(`PgCache: listening channels ${channels.join(', ')} on ${className}`); }); await this.pubSub.connect(); } } const proto = new CachedService(); for (const prop of Object.keys(proto)) { constructor.prototype[prop] = proto[prop]; } constructor.prototype.start = CachedService.prototype.start; return constructor; }; } //# sourceMappingURL=PgCache.js.map