@imqueue/pg-cache
Version:
PostgreSQL managed cache on Redis for @imqueue-based service methods
192 lines • 7.96 kB
JavaScript
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
;