import { describe, beforeEach, test, expect, jest } from '@jest/globals'; import { Knifecycle, constant } from 'knifecycle'; import initLRUPool, { type LRUPoolManagerService } from './lruPool.js'; import { type LogService } from './log.js'; import { DelayResult, type DelayService } from './delay.js'; const instances = ['i1', 'i2', 'i3', 'i4'] as const; type V = number; type U = (typeof instances)[V]; describe('initLRUPool', () => { const MAX_POOL_SIZE = 3; const POOL_TTL = 30000; const poolManager = { create: jest.fn['create']>(), release: jest.fn['release']>(), }; const delay = { create: jest.fn(), clear: jest.fn(), }; const log = jest.fn(); beforeEach(() => { log.mockReset(); delay.create.mockReset(); delay.clear.mockReset(); poolManager.create.mockReset(); poolManager.create.mockImplementation((c) => Promise.resolve(instances[c])); poolManager.release.mockReset(); }); test('should work', async () => { const lruPool = await initLRUPool({ MAX_POOL_SIZE, poolManager, delay, log, }); expect('function' === typeof lruPool.service.use).toBeTruthy(); expect({ delayCreateCalls: delay.create.mock.calls, delayClearCalls: delay.clear.mock.calls, logCalls: log.mock.calls, }).toMatchInlineSnapshot(` { "delayClearCalls": [], "delayCreateCalls": [], "logCalls": [ [ "warning", "โœ… - Initializing a LRU pool service.", ], ], } `); }); describe('lruPool', () => { test('should work without ttl', async () => { const lruPool = await initLRUPool({ MAX_POOL_SIZE, poolManager, delay, log, }); delay.create.mockImplementation(() => new Promise(() => undefined)); log.mockClear(); for (let i = 0; i < 6; i++) { const instance = await lruPool.service.use(i % instances.length); expect(instance).toBe(instances[i % instances.length]); } await lruPool.dispose(); expect({ delayCreateCalls: delay.create.mock.calls, delayClearCalls: delay.clear.mock.calls, logCalls: log.mock.calls, }).toMatchInlineSnapshot(` { "delayClearCalls": [], "delayCreateCalls": [], "logCalls": [ [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is full, releasing an item (0).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is full, releasing an item (1).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is full, releasing an item (2).", ], [ "debug", "โž• - Creating an item from the pool.", ], ], } `); }); }); test('should work with ttl', async () => { const lruPool = await initLRUPool({ MAX_POOL_SIZE, POOL_TTL, poolManager, delay, log, }); log.mockClear(); delay.create.mockImplementation(() => new Promise(() => undefined)); for (let i = 0; i < 6; i++) { const instance = await lruPool.service.use(i % instances.length); expect(instance).toBe(instances[i % instances.length]); } await lruPool.dispose(); expect({ delayCreateCalls: delay.create.mock.calls, delayClearCalls: delay.clear.mock.calls, logCalls: log.mock.calls, }).toMatchInlineSnapshot(` { "delayClearCalls": [ [ Promise {}, ], [ Promise {}, ], [ Promise {}, ], [ Promise {}, ], [ Promise {}, ], [ Promise {}, ], ], "delayCreateCalls": [ [ 30000, ], [ 30000, ], [ 30000, ], [ 30000, ], [ 30000, ], [ 30000, ], ], "logCalls": [ [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is full, releasing an item (0).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is full, releasing an item (1).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is full, releasing an item (2).", ], [ "debug", "โž• - Creating an item from the pool.", ], ], } `); }); test('should work with realized ttl', async () => { const lruPool = await initLRUPool({ MAX_POOL_SIZE, POOL_TTL, poolManager, delay, log, }); log.mockClear(); for (let i = 0; i < 6; i++) { const { promise, resolve } = Promise.withResolvers(); delay.create.mockReturnValueOnce(promise); const instance = await lruPool.service.use(i % instances.length); // @ts-expect-error Will always have a value resolve(); await promise; expect(instance).toBe(instances[i % instances.length]); } await lruPool.dispose(); expect({ delayCreateCalls: delay.create.mock.calls, delayClearCalls: delay.clear.mock.calls, logCalls: log.mock.calls, }).toMatchInlineSnapshot(` { "delayClearCalls": [], "delayCreateCalls": [ [ 30000, ], [ 30000, ], [ 30000, ], [ 30000, ], [ 30000, ], [ 30000, ], ], "logCalls": [ [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is releasing an item by ttl (0).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is releasing an item by ttl (1).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is releasing an item by ttl (2).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is releasing an item by ttl (3).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is releasing an item by ttl (0).", ], [ "debug", "โž• - Creating an item from the pool.", ], [ "debug", "๐Ÿงน - Pool is releasing an item by ttl (1).", ], ], } `); }); test('should work with Knifecycle', async () => { const { lruPool } = await new Knifecycle() .register(initLRUPool) .register(constant('MAX_POOL_SIZE', MAX_POOL_SIZE)) .register(constant('POOL_TTL', POOL_TTL)) .register(constant('delay', delay)) .register(constant('poolManager', poolManager)) .register(constant('log', log)) .run(['lruPool']); expect(lruPool).toBeDefined(); expect({ delayCreateCalls: delay.create.mock.calls, delayClearCalls: delay.clear.mock.calls, logCalls: log.mock.calls, }).toMatchInlineSnapshot(` { "delayClearCalls": [], "delayCreateCalls": [], "logCalls": [ [ "warning", "โœ… - Initializing a LRU pool service.", ], ], } `); }); });