Files
stats/packages/redis/cachable.ts
Carl-Gerhard Lindesvärd 168ebc3430 feat(subscriptions): added polar as payment provider for subscriptions
* feature(dashboard): add polar / subscription

* wip(payments): manage subscription

* wip(payments): add free product, faq and some other improvements

* fix(root): change node to bundler in tsconfig

* wip(payments): display current subscription

* feat(dashboard): schedule project for deletion

* wip(payments): support custom products/subscriptions

* wip(payments): fix polar scripts

* wip(payments): add json package to dockerfiles
2025-02-26 11:24:00 +01:00

93 lines
2.5 KiB
TypeScript

import { getRedisCache } from './redis';
export async function getCache<T>(
key: string,
expireInSec: number,
fn: () => Promise<T>,
): Promise<T> {
const hit = await getRedisCache().get(key);
if (hit) {
return JSON.parse(hit, (_, value) => {
if (
typeof value === 'string' &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/.test(value)
) {
return new Date(value);
}
return value;
});
}
const data = await fn();
await getRedisCache().setex(key, expireInSec, JSON.stringify(data));
return data;
}
export function cacheable<T extends (...args: any) => any>(
fn: T,
expireInSec: number,
) {
const cachePrefix = `cachable:${fn.name}`;
function stringify(obj: unknown): string {
if (obj === null) return 'null';
if (obj === undefined) return 'undefined';
if (typeof obj === 'boolean') return obj ? 'true' : 'false';
if (typeof obj === 'number') return String(obj);
if (typeof obj === 'string') return obj;
if (typeof obj === 'function') return obj.toString();
if (Array.isArray(obj)) {
return `[${obj.map(stringify).join(',')}]`;
}
if (typeof obj === 'object') {
const pairs = Object.entries(obj)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}:${stringify(value)}`);
return pairs.join(':');
}
// Fallback for any other types
return String(obj);
}
const getKey = (...args: Parameters<T>) =>
`${cachePrefix}:${stringify(args)}`;
const cachedFn = async (
...args: Parameters<T>
): Promise<Awaited<ReturnType<T>>> => {
// JSON.stringify here is not bullet proof since ordering of object keys matters etc
const key = getKey(...args);
const cached = await getRedisCache().get(key);
if (cached) {
try {
return JSON.parse(cached, (_, value) => {
if (
typeof value === 'string' &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/.test(value)
) {
return new Date(value);
}
return value;
});
} catch (e) {
console.error('Failed to parse cache', e);
}
}
const result = await fn(...(args as any));
if (result !== undefined || result !== null) {
getRedisCache().setex(key, expireInSec, JSON.stringify(result));
}
return result;
};
cachedFn.getKey = getKey;
cachedFn.clear = async (...args: Parameters<T>) => {
const key = getKey(...args);
return getRedisCache().del(key);
};
return cachedFn;
}