@imqueue/pg-cache
Version:
PostgreSQL managed cache on Redis for @imqueue-based service methods
133 lines • 5.93 kB
JavaScript
/*!
* 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.
*/
import { Model } from 'sequelize-typescript';
import { DEFAULT_CACHE_TTL, declaringPrototype, fetchError, initError, isStandardDecorator, registerChannelsOnce, setError, setInfo, } from './env.js';
import { signature } from './signature.js';
import { TagCache } from '@imqueue/tag-cache';
/**
* Retrieves table names as channels from the given model and filter them by
* a given fields map, if passed. Returns result as list of table names.
*
* @param model - sequelize-style model whose table name and associations are
* read to derive the channels
* @param fields - fields the cached method depends on; an association field
* pulls in that association's table as well
* @param tables - extra table names to watch that the model does not reach
*/
export function channelsOf(model, fields, tables = []) {
const modelRels = model.associations;
const relsMap = fields ? fields : model.associations;
const rels = Object.keys(relsMap);
const table = model.tableName;
tables.push(table);
for (const field of rels) {
if (!modelRels[field]) {
continue;
}
const relation = modelRels[field];
const { target, options } = relation;
const through = options && options.through && options.through.model;
const subFields = (fields || {})[field];
if (through && !~tables.indexOf(through.tableName)) {
channelsOf(through, subFields, tables);
}
if (target && !~tables.indexOf(target.tableName)) {
channelsOf(target, subFields, tables);
}
}
return tables;
}
/**
* Decorator factory `@cacheBy`(Model, CacheByOptions)
* This decorator should be used on a service methods, to set the caching
* rules for a method. Caching rules within this decorator are defined by a
* passed model, which is treated as a root model of the call and it analyzes
* cache invalidation based on passed runtime fields arguments, which
* prevents unnecessary cache invalidations. So it is more intellectual way
* to invalidate cache instead of any changes on described list of tables.
*/
export function cacheBy(model, options) {
const opts = options || {};
const ttl = opts.ttl || DEFAULT_CACHE_TTL;
const channels = channelsOf(model);
// registers this method's channel entries on the declaring prototype
const register = (proto, methodName) => registerChannelsOnce(proto, methodName, pgCacheChannels => {
for (const channel of channels) {
const pgChannel = (pgCacheChannels[channel] =
pgCacheChannels[channel] || []);
pgChannel.push([methodName]);
}
});
// builds the caching wrapper; `getClassName` is resolved lazily so it
// works in standard mode where the class is unknown at decoration time
const wrap = (original, methodName, getClassName, fallback) => async function (...args) {
const self = this || fallback;
const cache = self.taggedCache;
const logger = self.logger || console;
const className = getClassName();
if (!cache) {
initError(logger, className, methodName, cacheBy);
return original.apply(self, args);
}
const fields = args[opts.fieldsArg];
const key = signature(className, methodName, args);
try {
let result = await cache.get(key);
if (result === null || result === undefined) {
result = original.apply(self, args);
if (result && result.then) {
result = await result;
}
const tags = channelsOf(model, fields).map(table => signature(className, methodName, [table]));
cache
.set(key, result, tags, ttl)
.then(res => setInfo(logger, res, key, cacheBy))
.catch(err => setError(logger, err, key, cacheBy));
}
return result;
}
catch (err) {
fetchError(logger, err, key, cacheBy);
return original.apply(self, args);
}
};
return (target, context, descriptor) => {
if (isStandardDecorator(context)) {
const methodName = String(context.name);
let className = '';
context.addInitializer(function () {
const proto = declaringPrototype(this, methodName);
className = proto.constructor.name;
register(proto, methodName);
});
return wrap(target, methodName, () => className);
}
const methodName = String(context);
const className = typeof target === 'function'
? target.name
: target.constructor.name;
register(target, methodName);
descriptor.value = wrap(descriptor.value, methodName, () => className, target);
return descriptor;
};
}
//# sourceMappingURL=cacheBy.js.map