// npm import * as _ from 'lodash'; import { EventEmitter } from 'events'; // app import { ISegment } from '../playlist-builder'; import { ICacheManagerOptions, ICacheManager, CacheSegmentFilter, CacheManagerFactory, GetStorageError, GetMaxStorageError, SetMaxStorageError, WasBilledError, SaveCacheError, CheckCacheError, RemoveCacheError, ICacheManagerCheckSegmentInput, CheckCacheBatchError, } from '.'; import { log } from '../../config'; import { Nullable } from '../../utils/types'; export abstract class CacheManager extends EventEmitter implements ICacheManager { private static instance?: ICacheManager; private static readonly DEFAULT_BASE_OPTIONS: ICacheManagerOptions = { cleanupInterval: 60000, autoStartGC: false, }; public static getInstance(): ICacheManager { if (!CacheManager.instance) { CacheManager.instance = CacheManagerFactory.create(); } return CacheManager.instance; } public static destroyInstance(): void { CacheManager.instance = undefined; } public static assertGetStorageArguments(orgId: string): void|never { if (_.isEmpty(orgId)) { throw new GetStorageError('Invalid organization id provided.'); } } public static assertGetMaxStorageArguments(orgId: string): void|never { if (_.isEmpty(orgId)) { throw new GetMaxStorageError('Invalid organization id provided.'); } } public static assertSetMaxStorageArguments(orgId: string, maxStorage: number): void|never { if (_.isEmpty(orgId)) { throw new SetMaxStorageError('Invalid organization id provided.'); } if (_.isNaN(maxStorage) || maxStorage < 0) { throw new SetMaxStorageError(`Invalid max storage provided: "${maxStorage}". Please provide a positive numeric value.`); } } public static assertWasBilledArguments(orgId: string, fileId: string, segmentId: string): void|never { if (_.isEmpty(orgId)) { throw new WasBilledError('Invalid organization id provided.'); } if (_.isEmpty(fileId)) { throw new WasBilledError('Invalid file id provided.'); } if (_.isEmpty(segmentId)) { throw new WasBilledError('Invalid segment id provided.'); } } public static assertSaveCacheArguments(orgId: string, segmentId: string, segment: ISegment): void|never { if (_.isEmpty(orgId)) { throw new SaveCacheError('Invalid organization id provided.'); } if (_.isEmpty(segmentId)) { throw new SaveCacheError('Invalid segment id provided.'); } if (!_.isObject(segment)) { throw new SaveCacheError('Invalid segment object provided.'); } if (!segment.fileId) { throw new SaveCacheError('The provided segment must have a file id.'); } if (segment.orgId !== orgId) { throw new SaveCacheError(`The provided segment object has a different organization id than the one provided as argument. Argument OrgId: ${orgId}, Segment OrgId: ${segment.orgId}.`); } if (segment.id !== segmentId) { throw new SaveCacheError(`The provided segment object has a different id than the one provided as argument. Argument Segment Id: ${segmentId}, Segment Segment Id: ${segment.id as string}.`); } } public static assertCheckCacheArguments(orgId: string, fileId: string, segmentId: string): void|never { if (_.isEmpty(orgId)) { throw new CheckCacheError('Invalid organization id provided.'); } if (_.isEmpty(fileId)) { throw new CheckCacheError('Invalid file id provided.'); } if (_.isEmpty(segmentId)) { throw new CheckCacheError('Invalid segment id provided.'); } } public static assertCheckCacheBatchArguments(segments: ICacheManagerCheckSegmentInput[]): void { try { for (const segment of segments) { this.assertCheckCacheArguments(segment.orgId, segment.fileId, segment.segmentId); } } catch (error) { throw new CheckCacheBatchError((error as Error).message); } } public static assertRemoveCacheArguments(orgId: string, fileId: string, segmentId: string): void|never { if (_.isEmpty(orgId)) { throw new RemoveCacheError('Invalid organization id provided.'); } if (_.isEmpty(fileId)) { throw new RemoveCacheError('Invalid file id provided.'); } if (_.isEmpty(segmentId)) { throw new RemoveCacheError('Invalid segment id provided.'); } } public options: TOptions; protected interval: Nullable = null; protected timeout: Nullable = null; protected constructor(options: TOptions) { super(); this.options = _.defaultsDeep(options, CacheManager.DEFAULT_BASE_OPTIONS) as TOptions; if (this.options.autoStartGC) { this.startCacheGC(); } } public startCacheGC(): void { if (this.options.cleanupInterval === 0 || this.isGCEnabled()) { return; } log.debug(`${this.constructor.name}: startCacheGC`); this.timeout = setTimeout(() => { this.createInterval(); }, Math.random() * 10000); } public stopCacheGC(): void { log.debug(`${this.constructor.name}: stopCacheGC`); if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; if (this.interval) { clearInterval(this.interval); this.interval = null; } } } public abstract getStorage(orgId: string): Promise; public abstract getMaxStorage(orgId: string): Promise; public abstract setMaxStorage(orgId: string, maxStorage: number): Promise; public abstract wasBilled(orgId: string, fileId: string, segmentId: string): Promise; public abstract saveCache(orgId: string, segmentId: string, segment: ISegment): Promise; public abstract checkCache(orgId: string, fileId: string, segmentId: string): Promise; public abstract checkCacheBatch(segments: ICacheManagerCheckSegmentInput[]): Promise; public abstract removeCache(orgId: string, fileId: string, segmentId: string): Promise; public abstract clearCache(orgId?: string, files?: string[], chunkSize?: number, deletionParallelism?: number, filter?: CacheSegmentFilter, dryRun?: boolean): Promise; public abstract cleanupCache(): Promise; protected isGCEnabled(): boolean { return !!this.timeout; } private createInterval() { this.interval = setInterval(() => { void this.cleanupCache(); }, this.options.cleanupInterval); } }