import { autoProvider, name, location, ServiceProperties } from 'knifecycle'; import { noop } from '../utils/utils.js'; import { type LogService } from './log.js'; import { type JsonValue } from 'type-fest'; import { DelayResult, type DelayService } from './delay.js'; import { printStackTrace } from 'yerror'; export interface LRUPoolManagerService { create: (config: U) => Promise; release: (item: T) => Promise; } export interface LRUPoolServiceConfig { MAX_POOL_SIZE: number; POOL_TTL?: number; } export type LRUPoolServiceDependencies< T, U extends JsonValue, > = LRUPoolServiceConfig & { poolManager: LRUPoolManagerService; delay: DelayService; log?: LogService; }; export interface LRUPoolService { use: (config: U) => Promise; } export interface LRUPoolProvider { service: LRUPoolService; dispose: () => Promise; } /** * Instantiate the LRU Pool service * @name initRandom * @function * @param {Object} services The services to inject * @param {Object} [services.log = noop] A logging function * @return {Promise} A promise of the LRUPool service * @example * import { * DEFAULT_LOGGER, * initLog, * initLRUPool * } from 'common-services'; * * const log = await initLog({ * logger: DEFAULT_LOGGER, * }); * * const random = await initLRUPool({ * MAX_POOL_SIZE: 50, * poolManager: { * // ... * }, * log, * }); */ export async function initLRUPool({ MAX_POOL_SIZE, POOL_TTL, poolManager, delay, log = noop, }: LRUPoolServiceDependencies) { log('warning', `โœ… - Initializing a LRU pool service.`); const poolMap = new Map(); const delayMap = new Map>(); const poolLRUQueue: string[] = []; const service = { async use(config: U): Promise { if (poolLRUQueue.length >= MAX_POOL_SIZE) { const key = poolLRUQueue.shift() as string; const item = poolMap.get(key) as T; log('debug', `๐Ÿงน - Pool is full, releasing an item (${key}).`); await poolManager.release(item); poolMap.delete(key); if (delayMap.has(key)) { delay.clear(delayMap.get(key) as Promise); delayMap.delete(key); } } const key = JSON.stringify(config); let item: T; if (poolMap.has(key)) { item = poolMap.get(key) as T; poolLRUQueue.splice(poolLRUQueue.indexOf(key), 1); log('debug', `โ™ป๏ธ - Reusing an item from the pool (${key}).`); } else { log('debug', `โž• - Creating an item from the pool.`); item = await poolManager.create(config); poolMap.set(key, item); if (POOL_TTL) { const delayPromise = delay.create(POOL_TTL); delayMap.set(key, delayPromise); delayPromise .then((result) => { if (result === 'cancel') { return; } if (poolMap.has(key)) { poolLRUQueue.splice(poolLRUQueue.indexOf(key), 1); poolMap.delete(key); delayMap.delete(key); log('debug', `๐Ÿงน - Pool is releasing an item by ttl (${key}).`); return poolManager.release(item); } }) .catch((err) => { log('error', `๐Ÿ›‘ - Delayed release error (${key}).`); log('error-stack', printStackTrace(err)); }); } } poolLRUQueue.push(key); return item; }, }; const dispose = async () => { for (const item of poolMap.values()) { await poolManager.release(item); } poolMap.clear(); for (const item of delayMap.values()) { await delay.clear(item); } delayMap.clear(); poolLRUQueue.length = 0; }; return { service, dispose }; } /* Architecture Note #1.10: LRU Pool The `lruPool` service allows to maintain a pool of resources. It is meant to be used with resources like file descriptors that are limited in most OSes but are pointing to completely different kind of resources (files paths varies). */ export default location( name('lruPool', autoProvider(initLRUPool)), import.meta.url, ) as unknown as ServiceProperties & typeof initLRUPool;