refactor: consistently use cacheable for caching
This commit is contained in:
parent
50764b3602
commit
dc093f7109
5 changed files with 72 additions and 56 deletions
52
package-lock.json
generated
52
package-lock.json
generated
|
|
@ -9,16 +9,15 @@
|
|||
"version": "0.45.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bytes": "^3.1.2",
|
||||
"cacheable": "^1.10.4",
|
||||
"cheerio": "^1.0.0",
|
||||
"express": "^5.1.0",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-cache-semantics": "^4.2.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"lru-cache": "^11.1.0",
|
||||
"randomstring": "^1.3.1",
|
||||
"slugify": "^1.6.6",
|
||||
"tough-cookie": "^6.0.0",
|
||||
|
|
@ -1025,15 +1024,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/ttlcache": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
|
||||
"integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
|
|
@ -1442,6 +1432,12 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/serialize": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.0.tgz",
|
||||
"integrity": "sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
|
|
@ -3064,6 +3060,25 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable": {
|
||||
"version": "1.10.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.4.tgz",
|
||||
"integrity": "sha512-Gd7ccIUkZ9TE2odLQVS+PDjIvQCdJKUlLdJRVvZu0aipj07Qfx+XIej7hhDrKGGoIxV5m5fT/kOJNJPQhQneRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hookified": "^1.11.0",
|
||||
"keyv": "^5.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable/node_modules/keyv": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.0.tgz",
|
||||
"integrity": "sha512-QG7qR2tijh1ftOvClut4YKKg1iW6cx3GZsKoGyJPxHkGWK9oJhG9P3j5deP0QQOGDowBMVQFaP+Vm4NpGYvmIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@keyv/serialize": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
|
|
@ -5190,6 +5205,12 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hookified": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.0.tgz",
|
||||
"integrity": "sha512-hMr1Y9TCLshScrBbV2QxJ9BROddxZ12MX9KsCtuGGy/3SmmN5H1PllKerrVlSotur9dlE8hmUKAOSa3WDzsZmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
|
|
@ -6832,15 +6853,6 @@
|
|||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -31,16 +31,15 @@
|
|||
"node": "^22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bytes": "^3.1.2",
|
||||
"cacheable": "^1.10.4",
|
||||
"cheerio": "^1.0.0",
|
||||
"express": "^5.1.0",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-cache-semantics": "^4.2.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"lru-cache": "^11.1.0",
|
||||
"randomstring": "^1.3.1",
|
||||
"slugify": "^1.6.6",
|
||||
"tough-cookie": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import TTLCache from '@isaacs/ttlcache';
|
||||
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
|
||||
import winston from 'winston';
|
||||
import { Context, CountryCode, UrlResult } from '../types';
|
||||
import { isExtractorDisabled } from '../utils';
|
||||
|
|
@ -7,12 +7,13 @@ import { Extractor } from './Extractor';
|
|||
export class ExtractorRegistry {
|
||||
private readonly logger: winston.Logger;
|
||||
private readonly extractors: Extractor[];
|
||||
private readonly urlResultCache: TTLCache<string, UrlResult[]>;
|
||||
private readonly urlResultCache: Cacheable;
|
||||
|
||||
public constructor(logger: winston.Logger, extractors: Extractor[]) {
|
||||
this.logger = logger;
|
||||
this.extractors = extractors;
|
||||
this.urlResultCache = new TTLCache({ max: 4096 });
|
||||
|
||||
this.urlResultCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 4096 }) }) });
|
||||
}
|
||||
|
||||
public async handle(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
|
||||
|
|
@ -26,17 +27,17 @@ export class ExtractorRegistry {
|
|||
? `${extractor.id}_${normalizedUrl}_${ctx.config.mediaFlowProxyUrl}`
|
||||
: `${extractor.id}_${normalizedUrl}`;
|
||||
|
||||
let urlResults = this.urlResultCache.get(cacheKey) ?? [];
|
||||
if (this.urlResultCache.has(cacheKey)) {
|
||||
return urlResults.map(urlResult => ({ ...urlResult, ttl: this.urlResultCache.getRemainingTTL(cacheKey) }));
|
||||
const storedDataRaw = await this.urlResultCache.get<UrlResult[]>(cacheKey, { raw: true });
|
||||
if (storedDataRaw) {
|
||||
return (storedDataRaw.value as UrlResult[]).map(urlResult => ({ ...urlResult, ttl: storedDataRaw.expires as number - Date.now(), url: new URL(urlResult.url) }));
|
||||
}
|
||||
|
||||
this.logger.info(`Extract stream URL using ${extractor.id} extractor from ${url}`, ctx);
|
||||
|
||||
urlResults = await extractor.extract(ctx, normalizedUrl, countryCode, title);
|
||||
const urlResults = await extractor.extract(ctx, normalizedUrl, countryCode, title);
|
||||
|
||||
if (!urlResults.some(urlResult => urlResult.error) && extractor.ttl) {
|
||||
this.urlResultCache.set(cacheKey, urlResults, { ttl: extractor.ttl });
|
||||
await this.urlResultCache.set<UrlResult[]>(cacheKey, urlResults, extractor.ttl);
|
||||
}
|
||||
|
||||
return urlResults;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import TTLCache from '@isaacs/ttlcache';
|
||||
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
|
||||
import { ContentType } from 'stremio-addon-sdk';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, CountryCode } from '../types';
|
||||
|
|
@ -23,21 +23,22 @@ export abstract class Source {
|
|||
|
||||
public abstract readonly baseUrl: string;
|
||||
|
||||
private readonly sourceResultCache: TTLCache<string | number, SourceResult[]>;
|
||||
private readonly sourceResultCache: Cacheable;
|
||||
|
||||
public constructor() {
|
||||
this.sourceResultCache = new TTLCache({ max: 1024, ttl: this.ttl });
|
||||
this.sourceResultCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024, ttl: this.ttl }) }) });
|
||||
}
|
||||
|
||||
protected abstract handleInternal(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])>;
|
||||
|
||||
public async handle(ctx: Context, type: ContentType, id: Id): Promise<(SourceResult[])> {
|
||||
const cacheKey = id.toString();
|
||||
if (this.sourceResultCache.has(cacheKey)) {
|
||||
return this.sourceResultCache.get(cacheKey) as SourceResult[];
|
||||
|
||||
let sourceResults = await this.sourceResultCache.get<SourceResult[]>(cacheKey);
|
||||
if (sourceResults) {
|
||||
return sourceResults.map(sourceResult => ({ ...sourceResult, url: new URL(sourceResult.url) }));
|
||||
}
|
||||
|
||||
let sourceResults: SourceResult[];
|
||||
try {
|
||||
sourceResults = await this.handleInternal(ctx, type, id);
|
||||
} catch (error) {
|
||||
|
|
@ -48,7 +49,7 @@ export abstract class Source {
|
|||
}
|
||||
}
|
||||
|
||||
this.sourceResultCache.set(cacheKey, sourceResults);
|
||||
await this.sourceResultCache.set<SourceResult[]>(cacheKey, sourceResults);
|
||||
|
||||
return sourceResults;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { gunzipSync, gzipSync } from 'zlib';
|
||||
import TTLCache from '@isaacs/ttlcache';
|
||||
import { Mutex, Semaphore, SemaphoreInterface, withTimeout } from 'async-mutex';
|
||||
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
|
||||
import CachePolicy from 'http-cache-semantics';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { Cookie, CookieJar } from 'tough-cookie';
|
||||
import { fetch, Headers, RequestInit, Response } from 'undici';
|
||||
import winston from 'winston';
|
||||
|
|
@ -64,13 +63,13 @@ export class Fetcher {
|
|||
|
||||
private readonly logger: winston.Logger;
|
||||
|
||||
private readonly httpCache = new LRUCache<string, Buffer>({ max: 2048 });
|
||||
private readonly rateLimitedCache = new TTLCache<string, undefined>({ max: 1024 });
|
||||
private readonly httpCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 2048 }) }) });
|
||||
private readonly rateLimitedCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }) });
|
||||
private readonly semaphores = new Map<string, SemaphoreInterface>();
|
||||
private readonly hostUserAgentMap = new Map<string, string>();
|
||||
private readonly cookieJar = new CookieJar();
|
||||
|
||||
private readonly timeoutsCountCache = new TTLCache<string, number>({ max: 1024 });
|
||||
private readonly timeoutsCountCache = new Cacheable({ primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }) });
|
||||
private readonly timeoutsCountMutex = new Mutex();
|
||||
|
||||
public constructor(logger: winston.Logger) {
|
||||
|
|
@ -178,7 +177,7 @@ export class Fetcher {
|
|||
if (httpCacheItem.status === 429) {
|
||||
const retryAfter = parseInt(`${httpCacheItem.headers['retry-after']}`);
|
||||
if (!isNaN(retryAfter)) {
|
||||
this.rateLimitedCache.set(url.host, undefined, { ttl: retryAfter * 1000 });
|
||||
await this.rateLimitedCache.set<true>(url.host, true, retryAfter);
|
||||
}
|
||||
|
||||
throw new TooManyRequestsError(retryAfter);
|
||||
|
|
@ -199,18 +198,18 @@ export class Fetcher {
|
|||
return policy.timeToLive();
|
||||
};
|
||||
|
||||
private cacheGet(key: string): HttpCacheItem | undefined {
|
||||
const buffer = this.httpCache.get(key);
|
||||
private async cacheGet(key: string): Promise<HttpCacheItem | undefined> {
|
||||
const buffer = await this.httpCache.get<Buffer>(key);
|
||||
|
||||
return buffer ? JSON.parse(gunzipSync(buffer).toString()) : undefined;
|
||||
}
|
||||
|
||||
private cacheSet(key: string, httpCacheItem: HttpCacheItem) {
|
||||
private async cacheSet(key: string, httpCacheItem: HttpCacheItem) {
|
||||
if (httpCacheItem.ttl <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.httpCache.set(key, gzipSync(JSON.stringify(httpCacheItem)), { ttl: httpCacheItem.ttl });
|
||||
await this.httpCache.set<Buffer>(key, gzipSync(JSON.stringify(httpCacheItem)), httpCacheItem.ttl);
|
||||
}
|
||||
|
||||
private headersToObject(headers: Headers): Record<string, string> {
|
||||
|
|
@ -229,7 +228,7 @@ export class Fetcher {
|
|||
const request: CachePolicy.Request = { url: url.href, method: newInit.method ?? 'GET', headers: {} };
|
||||
|
||||
const cacheKey = this.determineCacheKey(url, init);
|
||||
let httpCacheItem = this.cacheGet(cacheKey);
|
||||
let httpCacheItem = await this.cacheGet(cacheKey);
|
||||
const noCache = init?.noCache ?? false;
|
||||
if (httpCacheItem && !noCache) {
|
||||
this.logger.info(`Cached fetch ${request.method} ${url}`, ctx);
|
||||
|
|
@ -244,7 +243,7 @@ export class Fetcher {
|
|||
|
||||
httpCacheItem = { headers: policy.responseHeaders(), status: response.status, statusText: response.statusText, body, ttl };
|
||||
|
||||
this.cacheSet(cacheKey, httpCacheItem);
|
||||
await this.cacheSet(cacheKey, httpCacheItem);
|
||||
|
||||
return this.handleHttpCacheItem(ctx, httpCacheItem, url, init);
|
||||
};
|
||||
|
|
@ -252,11 +251,13 @@ export class Fetcher {
|
|||
protected async fetchWithTimeout(ctx: Context, url: URL, init?: CustomRequestInit, tryCount = 0): Promise<Response> {
|
||||
this.logger.info(`Fetch ${init?.method ?? 'GET'} ${url}`, ctx);
|
||||
|
||||
if (this.rateLimitedCache.has(url.host)) {
|
||||
throw new TooManyRequestsError(this.rateLimitedCache.getRemainingTTL(url.host) / 1000);
|
||||
const isRateLimitedRaw = await this.rateLimitedCache.get<true>(url.host, { raw: true });
|
||||
if (isRateLimitedRaw && isRateLimitedRaw.value) {
|
||||
throw new TooManyRequestsError((isRateLimitedRaw.expires as number - Date.now()) / 1000);
|
||||
}
|
||||
|
||||
if ((this.timeoutsCountCache.get(url.host) ?? 0) >= (init?.timeoutsCountThrow ?? this.DEFAULT_TIMEOUTS_COUNT_THROW)) {
|
||||
const timeouts = (await this.timeoutsCountCache.get<number>(url.host)) ?? 0;
|
||||
if (timeouts >= (init?.timeoutsCountThrow ?? this.DEFAULT_TIMEOUTS_COUNT_THROW)) {
|
||||
throw new TooManyTimeoutsError();
|
||||
}
|
||||
|
||||
|
|
@ -299,15 +300,17 @@ export class Fetcher {
|
|||
|
||||
private async increaseTimeoutsCount(url: URL) {
|
||||
await this.timeoutsCountMutex.runExclusive(async () => {
|
||||
const count = (this.timeoutsCountCache.get(url.host) ?? 0) + 1;
|
||||
this.timeoutsCountCache.set(url.host, count, { ttl: this.TIMEOUT_CACHE_TTL });
|
||||
const count = (await this.timeoutsCountCache.get<number>(url.host)) ?? 0;
|
||||
const newCount = count + 1;
|
||||
await this.timeoutsCountCache.set<number>(url.host, newCount, this.TIMEOUT_CACHE_TTL);
|
||||
});
|
||||
}
|
||||
|
||||
private async decreaseTimeoutsCount(url: URL) {
|
||||
await this.timeoutsCountMutex.runExclusive(async () => {
|
||||
const count = Math.max(0, (this.timeoutsCountCache.get(url.host) ?? 0) - 1);
|
||||
this.timeoutsCountCache.set(url.host, count, { ttl: this.TIMEOUT_CACHE_TTL });
|
||||
const count = (await this.timeoutsCountCache.get<number>(url.host)) ?? 0;
|
||||
const newCount = Math.max(0, count - 1);
|
||||
await this.timeoutsCountCache.set<number>(url.host, newCount, this.TIMEOUT_CACHE_TTL);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue