You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

138 lines
3.3 KiB

import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier, Filter } from '@ghostfolio/common/interfaces';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { Inject, Injectable, Logger } from '@nestjs/common';
import Keyv from 'keyv';
import ms from 'ms';
import { createHash } from 'node:crypto';
@Injectable()
export class RedisCacheService {
private client: Keyv;
public constructor(
@Inject(CACHE_MANAGER) private readonly cache: Cache,
private readonly configurationService: ConfigurationService
) {
this.client = cache.stores[0];
this.client.deserialize = (value) => {
try {
return JSON.parse(value);
} catch {}
return value;
};
this.client.on('error', (error) => {
Logger.error(error, 'RedisCacheService');
});
}
public async get(key: string): Promise<string> {
return this.cache.get(key);
}
public async getKeys(aPrefix?: string): Promise<string[]> {
const keys: string[] = [];
const prefix = aPrefix;
try {
for await (const [key] of this.client.iterator({})) {
if ((prefix && key.startsWith(prefix)) || !prefix) {
keys.push(key);
}
}
} catch {}
return keys;
}
public getPortfolioSnapshotKey({
filters,
userId
}: {
filters?: Filter[];
userId: string;
}) {
let portfolioSnapshotKey = `portfolio-snapshot-${userId}`;
if (filters?.length > 0) {
const filtersHash = createHash('sha256')
.update(JSON.stringify(filters))
.digest('hex');
portfolioSnapshotKey = `${portfolioSnapshotKey}-${filtersHash}`;
}
return portfolioSnapshotKey;
}
public getQuoteKey({ dataSource, symbol }: AssetProfileIdentifier) {
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
}
public async isHealthy() {
const testKey = '__health_check__';
const testValue = Date.now().toString();
try {
await Promise.race([
(async () => {
await this.set(testKey, testValue, ms('1 second'));
const result = await this.get(testKey);
if (result !== testValue) {
throw new Error('Redis health check failed: value mismatch');
}
})(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error('Redis health check failed: timeout')),
ms('2 seconds')
)
)
]);
return true;
} catch (error) {
Logger.error(error?.message, 'RedisCacheService');
return false;
} finally {
try {
await this.remove(testKey);
} catch {}
}
}
public async remove(key: string) {
return this.cache.del(key);
}
public async removePortfolioSnapshotsByUserId({
userId
}: {
userId: string;
}) {
const keys = await this.getKeys(
`${this.getPortfolioSnapshotKey({ userId })}`
);
return this.cache.mdel(keys);
}
public async reset() {
return this.cache.clear();
}
public async set(key: string, value: string, ttl?: number) {
return this.cache.set(
key,
value,
ttl ?? this.configurationService.get('CACHE_TTL')
);
}
}