@imqueue/pg-cache
Version:
PostgreSQL managed cache on Redis for @imqueue-based service methods
112 lines • 4.89 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 { signature } from './signature.js';
import { TagCache } from '@imqueue/tag-cache';
import {} from './PgCache.js';
import { DEFAULT_CACHE_TTL, declaringPrototype, fetchError, initError, isStandardDecorator, registerChannelsOnce, setError, setInfo, } from './env.js';
/**
* Makes channel entry from a given channel name, class method name and options.
*
* @param name - notification channel, i.e. the watched table name
* @param method - decorated method to invalidate when that channel fires
* @param options - which channels invalidate the result, its TTL and cache tag decorator options; only the per-channel filter is read here
*/
export function makeChannel(name, method, options) {
return [
method,
!Array.isArray(options.channels) ? options.channels[name] : undefined,
];
}
/**
* Decorator factory `@cacheWith`(CacheWithOptions)
* This decorator should be used on a service methods, to set the caching
* rules for a method.
*
* @param options - which channels invalidate the result, its TTL and cache tag
*/
export function cacheWith(options) {
const ttl = options.ttl || DEFAULT_CACHE_TTL;
const isFiltered = !Array.isArray(options.channels);
const channels = isFiltered
? Object.keys(options.channels || {})
: options.channels;
// 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(makeChannel(channel, methodName, options));
}
});
// 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, cacheWith);
return original.apply(self, args);
}
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 = [signature(className, methodName, [])];
cache
.set(key, result, tags, ttl)
.then(res => setInfo(logger, res, key, cacheWith))
.catch(err => setError(logger, err, key, cacheWith));
}
return result;
}
catch (err) {
fetchError(logger, err, key, cacheWith);
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=cacheWith.js.map