diff --git a/packages/agent/src/prototyping/clients/dwn-server-info-cache-level.ts b/packages/agent/src/prototyping/clients/dwn-server-info-cache-level.ts deleted file mode 100644 index 6cc0551d4..000000000 --- a/packages/agent/src/prototyping/clients/dwn-server-info-cache-level.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type { AbstractLevel } from 'abstract-level'; - -import ms from 'ms'; -import { Level } from 'level'; -import { DwnServerInfoCache, ServerInfo } from './server-info-types.js'; - -/** - * Configuration parameters for creating a LevelDB-based cache for DWN Server Info results. - * - * Allows customization of the underlying database instance, storage location, and cache - * time-to-live (TTL) settings. - */ -export type DwnServerCacheLevelParams = { - /** - * Optional. An instance of `AbstractLevel` to use as the database. If not provided, a new - * LevelDB instance will be created at the specified `location`. - */ - db?: AbstractLevel; - - /** - * Optional. The file system path or IndexedDB name where the LevelDB store will be created. - * Defaults to 'DATA/DWN_SERVERINFOCACHE' if not specified. - */ - location?: string; - - /** - * Optional. The time-to-live for cache entries, expressed as a string (e.g., '1h', '15m'). - * Determines how long a cache entry should remain valid before being considered expired. Defaults - * to '15m' if not specified. - */ - ttl?: string; -} - -/** - * Encapsulates a ServerInfo result along with its expiration information for caching purposes. - * - * This type is used internally by the `DwnServerInfoCacheLevel` to store DWN ServerInfo results - * with an associated time-to-live (TTL) value. The TTL is represented in milliseconds and - * determines when the cached entry is considered expired and eligible for removal. - */ -type CacheWrapper = { - /** - * The expiration time of the cache entry in milliseconds since the Unix epoch. - * - * This value is used to calculate whether the cached entry is still valid or has expired. - */ - ttlMillis: number; - - /** - * The DWN ServerInfo entry being cached. - */ - value: ServerInfo; -} - -/** - * A Level-based cache implementation for storing and retrieving DWN ServerInfo results. - * - * This cache uses LevelDB for storage, allowing data persistence across process restarts or - * browser refreshes. It's suitable for both Node.js and browser environments. - * - * @remarks - * The LevelDB cache keeps data in memory for fast access and also writes to the filesystem in - * Node.js or indexedDB in browsers. Time-to-live (TTL) for cache entries is configurable. - * - * @example - * ``` - * const cache = new DwnServerInfoCacheLevel({ ttl: '15m' }); - * ``` - */ -export class DwnServerInfoCacheLevel implements DwnServerInfoCache { - /** The underlying LevelDB store used for caching. */ - private cache: AbstractLevel; - - /** The time-to-live for cache entries in milliseconds. */ - private ttl: number; - - constructor({ - db, - location = 'DATA/DWN_SERVERINFOCACHE', - ttl = '15m' - }: DwnServerCacheLevelParams = {}) { - this.cache = db ?? new Level(location); - this.ttl = ms(ttl); - } - - /** - * Retrieves a DWN ServerInfo entry from the cache. - * - * If the cached item has exceeded its TTL, it's scheduled for deletion and undefined is returned. - * - * @param dwnUrl - The DWN URL endpoint string used as the key for retrieving the cached result. - * @returns The cached ServerInfo entry or undefined if not found or expired. - */ - async get(dwnUrl: string): Promise { - try { - const str = await this.cache.get(dwnUrl); - const cacheWrapper: CacheWrapper = JSON.parse(str); - - if (Date.now() >= cacheWrapper.ttlMillis) { - // defer deletion to be called in the next tick of the js event loop - this.cache.nextTick(() => this.cache.del(dwnUrl)); - - return; - } else { - return cacheWrapper.value; - } - - } catch(error: any) { - // Don't throw when a key wasn't found. - if (error.notFound) { - return; - } - - throw error; - } - } - - /** - * Stores a DWN ServerInfo entry in the cache with a TTL. - * - * @param dwnUrl - The DWN URL endpoint string used as the key for storing the result. - * @param value - The DWN ServerInfo entry to be cached. - * @returns A promise that resolves when the operation is complete. - */ - set(dwnUrl: string, value: ServerInfo): Promise { - const cacheWrapper: CacheWrapper = { ttlMillis: Date.now() + this.ttl, value }; - const str = JSON.stringify(cacheWrapper); - - return this.cache.put(dwnUrl, str); - } - - /** - * Deletes a DWN ServerInfo entry from the cache. - * - * @param dwnUrl - The DWN URL endpoint string used as the key deletion. - * @returns A promise that resolves when the operation is complete. - */ - delete(dwnUrl: string): Promise { - return this.cache.del(dwnUrl); - } - - /** - * Clears all entries from the cache. - * - * @returns A promise that resolves when the operation is complete. - */ - clear(): Promise { - return this.cache.clear(); - } - - /** - * Closes the underlying LevelDB store. - * - * @returns A promise that resolves when the store is closed. - */ - close(): Promise { - return this.cache.close(); - } -} \ No newline at end of file diff --git a/packages/agent/src/prototyping/clients/dwn-server-info-cache-no-op.ts b/packages/agent/src/prototyping/clients/dwn-server-info-cache-no-op.ts deleted file mode 100644 index 3705f1646..000000000 --- a/packages/agent/src/prototyping/clients/dwn-server-info-cache-no-op.ts +++ /dev/null @@ -1,17 +0,0 @@ - -import { DwnServerInfoCache, ServerInfo } from './server-info-types.js'; - -export class DwnServerInfoCacheNoOp implements DwnServerInfoCache { - - public async get(_dwnUrl: string): Promise { - return; - } - - public async set(_dwnUrl: string, _value: ServerInfo): Promise {} - - public async delete(_dwnUrl: string): Promise {} - - public async clear(): Promise {} - - public async close(): Promise {} -} \ No newline at end of file diff --git a/packages/agent/tests/prototyping/clients/dwn-server-info-cache.spec.ts b/packages/agent/tests/prototyping/clients/dwn-server-info-cache.spec.ts index aea57abdd..d8c2b1733 100644 --- a/packages/agent/tests/prototyping/clients/dwn-server-info-cache.spec.ts +++ b/packages/agent/tests/prototyping/clients/dwn-server-info-cache.spec.ts @@ -4,161 +4,116 @@ import { expect } from 'chai'; import { DwnServerInfoCache, ServerInfo } from '../../../src/prototyping/clients/server-info-types.js'; import { DwnServerInfoCacheMemory } from '../../../src/prototyping/clients/dwn-server-info-cache-memory.js'; -import { DwnServerInfoCacheLevel } from '../../../src/prototyping/clients/dwn-server-info-cache-level.js'; -import { DwnServerInfoCacheNoOp } from '../../../src/prototyping/clients/dwn-server-info-cache-no-op.js'; -import { AbstractLevel } from 'abstract-level'; import { isNode } from '../../utils/runtimes.js'; describe('DwnServerInfoCache', () => { - const cacheImplementations = [ DwnServerInfoCacheMemory, DwnServerInfoCacheLevel ]; - - // basic cache tests for all caching interface implementations - for (const Cache of cacheImplementations) { - describe(`interface ${Cache.name}`, () => { - let cache: DwnServerInfoCache; - let clock: sinon.SinonFakeTimers; - - const exampleInfo:ServerInfo = { - maxFileSize : 100, - webSocketSupport : true, - registrationRequirements : [] - }; - - after(() => { - sinon.restore(); - }); - - beforeEach(() => { - clock = sinon.useFakeTimers(); - cache = new Cache(); - }); - - afterEach(async () => { - await cache.clear(); - await cache.close(); - clock.restore(); - }); - - it('sets server info in cache', async () => { - const key1 = 'some-key1'; - const key2 = 'some-key2'; - await cache.set(key1, { ...exampleInfo }); - await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false - - const result1 = await cache.get(key1); - expect(result1!.webSocketSupport).to.deep.equal(true); - expect(result1).to.deep.equal(exampleInfo); - - const result2 = await cache.get(key2); - expect(result2!.webSocketSupport).to.deep.equal(false); - }); - - it('deletes from cache', async () => { - const key1 = 'some-key1'; - const key2 = 'some-key2'; - await cache.set(key1, { ...exampleInfo }); - await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false - - const result1 = await cache.get(key1); - expect(result1!.webSocketSupport).to.deep.equal(true); - expect(result1).to.deep.equal(exampleInfo); - - const result2 = await cache.get(key2); - expect(result2!.webSocketSupport).to.deep.equal(false); - - // delete one of the keys - await cache.delete(key1); - - // check results after delete - const resultAfterDelete = await cache.get(key1); - expect(resultAfterDelete).to.equal(undefined); - - // key 2 still exists - const result2AfterDelete = await cache.get(key2); - expect(result2AfterDelete!.webSocketSupport).to.equal(false); - }); - - it('clears cache', async () => { - const key1 = 'some-key1'; - const key2 = 'some-key2'; - await cache.set(key1, { ...exampleInfo }); - await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false - - const result1 = await cache.get(key1); - expect(result1!.webSocketSupport).to.deep.equal(true); - expect(result1).to.deep.equal(exampleInfo); - - const result2 = await cache.get(key2); - expect(result2!.webSocketSupport).to.deep.equal(false); - - // delete one of the keys - await cache.clear(); - - // check results after delete - const resultAfterDelete = await cache.get(key1); - expect(resultAfterDelete).to.equal(undefined); - const result2AfterDelete = await cache.get(key2); - expect(result2AfterDelete).to.equal(undefined); - }); - - it('returns undefined after ttl', async function () { - // skip this test in the browser, sinon fake timers don't seem to work here - // with a an await setTimeout in the test, it passes. - if (!isNode) { - this.skip(); - } - - const key = 'some-key1'; - await cache.set(key, { ...exampleInfo }); - - const result = await cache.get(key); - expect(result!.webSocketSupport).to.deep.equal(true); - expect(result).to.deep.equal(exampleInfo); - - // wait until 15m default ttl is up - await clock.tickAsync('15:01'); - - const resultAfter = await cache.get(key); - expect(resultAfter).to.be.undefined; - }); - }); - } - - describe('DwnServerInfoCacheLevel', () => { - it('should throw on unknown level error', async () => { - const mockLevel = sinon.createStubInstance(AbstractLevel); - mockLevel.get.throws('test error'); - const cache = new DwnServerInfoCacheLevel({ db: mockLevel }); - - try { - await cache.get('key'); - expect.fail('Expected an error to be thrown'); - } catch(error: any) { - expect(error.message).to.contain('test error'); - } + describe(`DwnServerInfoCacheMemory`, () => { + let cache: DwnServerInfoCache; + let clock: sinon.SinonFakeTimers; + + const exampleInfo:ServerInfo = { + maxFileSize : 100, + webSocketSupport : true, + registrationRequirements : [] + }; + + after(() => { + sinon.restore(); }); - }); - describe('DwnServerInfoCacheNoOp', () => { - // for test coverage - const cache = new DwnServerInfoCacheNoOp(); + beforeEach(() => { + clock = sinon.useFakeTimers(); + cache = new DwnServerInfoCacheMemory(); + }); - it('sets', async () => { - await cache.set('test', { - webSocketSupport : true, - maxFileSize : 100, - registrationRequirements : [] - }); + afterEach(async () => { + await cache.clear(); + await cache.close(); + clock.restore(); }); - it('gets', async () => { - await cache.get('test'); + + it('sets server info in cache', async () => { + const key1 = 'some-key1'; + const key2 = 'some-key2'; + await cache.set(key1, { ...exampleInfo }); + await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false + + const result1 = await cache.get(key1); + expect(result1!.webSocketSupport).to.deep.equal(true); + expect(result1).to.deep.equal(exampleInfo); + + const result2 = await cache.get(key2); + expect(result2!.webSocketSupport).to.deep.equal(false); }); - it('delete', async () => { - await cache.delete('test'); + + it('deletes from cache', async () => { + const key1 = 'some-key1'; + const key2 = 'some-key2'; + await cache.set(key1, { ...exampleInfo }); + await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false + + const result1 = await cache.get(key1); + expect(result1!.webSocketSupport).to.deep.equal(true); + expect(result1).to.deep.equal(exampleInfo); + + const result2 = await cache.get(key2); + expect(result2!.webSocketSupport).to.deep.equal(false); + + // delete one of the keys + await cache.delete(key1); + + // check results after delete + const resultAfterDelete = await cache.get(key1); + expect(resultAfterDelete).to.equal(undefined); + + // key 2 still exists + const result2AfterDelete = await cache.get(key2); + expect(result2AfterDelete!.webSocketSupport).to.equal(false); }); - it('clear', async () => { + + it('clears cache', async () => { + const key1 = 'some-key1'; + const key2 = 'some-key2'; + await cache.set(key1, { ...exampleInfo }); + await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false + + const result1 = await cache.get(key1); + expect(result1!.webSocketSupport).to.deep.equal(true); + expect(result1).to.deep.equal(exampleInfo); + + const result2 = await cache.get(key2); + expect(result2!.webSocketSupport).to.deep.equal(false); + + // delete one of the keys await cache.clear(); + + // check results after delete + const resultAfterDelete = await cache.get(key1); + expect(resultAfterDelete).to.equal(undefined); + const result2AfterDelete = await cache.get(key2); + expect(result2AfterDelete).to.equal(undefined); + }); + + it('returns undefined after ttl', async function () { + // skip this test in the browser, sinon fake timers don't seem to work here + // with a an await setTimeout in the test, it passes. + if (!isNode) { + this.skip(); + } + + const key = 'some-key1'; + await cache.set(key, { ...exampleInfo }); + + const result = await cache.get(key); + expect(result!.webSocketSupport).to.deep.equal(true); + expect(result).to.deep.equal(exampleInfo); + + // wait until 15m default ttl is up + await clock.tickAsync('15:01'); + + const resultAfter = await cache.get(key); + expect(resultAfter).to.be.undefined; }); }); }); \ No newline at end of file