UNPKG

@imqueue/pg-cache

Version:

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

133 lines 5.93 kB
/*! * 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