@internetarchive/collection-name-cache
Advanced tools
Comparing version 0.0.1-alpha.5 to 0.0.1-alpha.6
@@ -41,5 +41,7 @@ import { SearchServiceInterface } from '@internetarchive/search-service'; | ||
private pruningAge; | ||
private maxCacheSize; | ||
private cacheLoaded; | ||
constructor(options: { | ||
searchService: SearchServiceInterface; | ||
maxCacheSize?: number; | ||
localCache?: LocalCacheInterface; | ||
@@ -54,2 +56,3 @@ loadDelay?: number; | ||
pruneCache(): Promise<void>; | ||
private persistCache; | ||
} |
@@ -6,3 +6,3 @@ /* eslint-disable camelcase */ | ||
constructor(options) { | ||
var _a, _b, _c; | ||
var _a, _b, _c, _d; | ||
this.cacheKeyName = 'collection-name-cache'; | ||
@@ -15,3 +15,3 @@ this.cacheTtl = 60 * 60 * 24 * 7; | ||
this.defaultPruningAge = 1000 * 60 * 60 * 24 * 7; | ||
this.defaultPruningInterval = 1000 * 10; | ||
this.defaultPruningInterval = 1000 * 30; | ||
this.fetchTimeout = null; | ||
@@ -22,2 +22,3 @@ this.pendingIdentifierQueue = new Set(); | ||
this.pruningAge = this.defaultPruningAge; | ||
this.maxCacheSize = 2500; | ||
this.cacheLoaded = false; | ||
@@ -28,6 +29,7 @@ this.searchService = options.searchService; | ||
this.pruningAge = (_b = options.pruningAge) !== null && _b !== void 0 ? _b : this.pruningAge; | ||
this.maxCacheSize = (_c = options.maxCacheSize) !== null && _c !== void 0 ? _c : this.maxCacheSize; | ||
this.pruneCache(); | ||
setInterval(async () => { | ||
await this.loadFromCache(); | ||
await this.pruneCache(); | ||
}, (_c = options.pruneInterval) !== null && _c !== void 0 ? _c : this.defaultPruningInterval); | ||
}, (_d = options.pruneInterval) !== null && _d !== void 0 ? _d : this.defaultPruningInterval); | ||
} | ||
@@ -87,3 +89,3 @@ /** @inheritdoc */ | ||
async loadPendingIdentifiers() { | ||
var _a, _b, _c, _d; | ||
var _a, _b, _c; | ||
await this.loadFromCache(); | ||
@@ -139,13 +141,17 @@ const pendingIdentifiers = Array.from(this.pendingIdentifierQueue).splice(0, 100); | ||
} | ||
await ((_d = this.localCache) === null || _d === void 0 ? void 0 : _d.set({ | ||
key: this.cacheKeyName, | ||
value: this.collectionNameCache, | ||
ttl: this.cacheTtl, | ||
})); | ||
await this.persistCache(); | ||
} | ||
// prune entries from the cache | ||
async pruneCache() { | ||
var _a; | ||
// prune old entries from the cache | ||
await this.loadFromCache(); | ||
const now = Date.now(); | ||
for (const [identifier, storageInfo] of Object.entries(this.collectionNameCache)) { | ||
// sorting the keys by lastAccess ascending so we can remove the oldest | ||
const sortedCache = Object.entries(this.collectionNameCache).sort((a, b) => { | ||
var _a, _b, _c, _d; | ||
const aLastAccess = (_b = (_a = a[1]) === null || _a === void 0 ? void 0 : _a.lastAccess) !== null && _b !== void 0 ? _b : 0; | ||
const bLastAccess = (_d = (_c = b[1]) === null || _c === void 0 ? void 0 : _c.lastAccess) !== null && _d !== void 0 ? _d : 0; | ||
return aLastAccess - bLastAccess; | ||
}); | ||
const identifiersToDelete = new Set(); | ||
for (const [identifier, storageInfo] of sortedCache) { | ||
if (!storageInfo) | ||
@@ -155,5 +161,20 @@ continue; | ||
if (lastAccess < now - this.pruningAge) { | ||
delete this.collectionNameCache[identifier]; | ||
identifiersToDelete.add(identifier); | ||
} | ||
} | ||
// delete oldest identifiers if number is greater than maxCacheSize | ||
if (sortedCache.length > this.maxCacheSize) { | ||
for (let i = 0; i < sortedCache.length - this.maxCacheSize; i += 1) { | ||
const [identifier] = sortedCache[i]; | ||
identifiersToDelete.add(identifier); | ||
} | ||
} | ||
// delete the identifiers from the cache | ||
for (const identifier of identifiersToDelete) { | ||
delete this.collectionNameCache[identifier]; | ||
} | ||
await this.persistCache(); | ||
} | ||
async persistCache() { | ||
var _a; | ||
await ((_a = this.localCache) === null || _a === void 0 ? void 0 : _a.set({ | ||
@@ -160,0 +181,0 @@ key: this.cacheKeyName, |
import { expect } from '@open-wc/testing'; | ||
import { CollectionNameCache } from '../src/collection-name-cache'; | ||
import { MockLocalCache } from './mocks/mock-local-cache'; | ||
import { mockSearchResponse, mockSearchResponseOnlyFoo, } from './mocks/mock-search-response'; | ||
@@ -170,3 +171,3 @@ import { MockSearchService } from './mocks/mock-search-service'; | ||
pruneInterval: 20, | ||
pruningAge: 75, | ||
pruningAge: 80, | ||
}); | ||
@@ -190,3 +191,80 @@ await collectionNameFetcher.preloadIdentifiers([ | ||
}); | ||
it('removes old items if caches gets too big', async () => { | ||
const mockSearchService = new MockSearchService(); | ||
mockSearchService.searchResult = mockSearchResponse; | ||
const collectionNameFetcher = new CollectionNameCache({ | ||
searchService: mockSearchService, | ||
loadDelay: 110, | ||
pruneInterval: 150, | ||
maxCacheSize: 2, | ||
}); | ||
// add some time in-between so the timestamps aren't all identical | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
await promisedSleep(50); | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
await promisedSleep(50); | ||
await collectionNameFetcher.collectionNameFor('baz-collection'); | ||
expect(mockSearchService.searchCallCount).to.equal(1); | ||
// waiting 60ms for the pruner to come through and prune the cache, which should remove the first item | ||
// since our max size is 2 | ||
await promisedSleep(60); | ||
// first check the bar-collection, which should not have been pruned so we still only have 1 request | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
expect(mockSearchService.searchCallCount).to.equal(1); | ||
// now we're going to fetch the one that should have been pruned so we should see another request | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
// wait to make sure the load delay elapses | ||
await promisedSleep(120); | ||
// and another request had to be made | ||
expect(mockSearchService.searchCallCount).to.equal(2); | ||
}); | ||
it('can persist the cache to localCache', async () => { | ||
const mockLocalCache = new MockLocalCache(); | ||
const mockSearchService = new MockSearchService(); | ||
mockSearchService.searchResult = mockSearchResponse; | ||
const collectionNameFetcher = new CollectionNameCache({ | ||
searchService: mockSearchService, | ||
localCache: mockLocalCache, | ||
loadDelay: 25, | ||
}); | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
await collectionNameFetcher.collectionNameFor('baz-collection'); | ||
// wait for the load to occur | ||
await promisedSleep(50); | ||
expect(mockLocalCache.storage['collection-name-cache']['bar-collection'].name).to.equal('Bar Collection'); | ||
expect(mockLocalCache.storage['collection-name-cache']['foo-collection'].name).to.equal('Foo Collection'); | ||
expect(mockLocalCache.storage['collection-name-cache']['baz-collection'].name).to.equal('Baz Collection'); | ||
}); | ||
it('will use localCache data if available', async () => { | ||
const mockLocalCache = new MockLocalCache(); | ||
mockLocalCache.storage['collection-name-cache'] = { | ||
'foo-collection': { | ||
name: 'Foo Collection', | ||
timestamp: Date.now(), | ||
}, | ||
'bar-collection': { | ||
name: 'Bar Collection', | ||
lastAccess: Date.now(), | ||
}, | ||
}; | ||
const mockSearchService = new MockSearchService(); | ||
mockSearchService.searchResult = mockSearchResponse; | ||
const collectionNameFetcher = new CollectionNameCache({ | ||
searchService: mockSearchService, | ||
localCache: mockLocalCache, | ||
loadDelay: 25, | ||
}); | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
await promisedSleep(50); | ||
expect(mockSearchService.searchCallCount).to.equal(0); | ||
// this is not in the cache | ||
await collectionNameFetcher.collectionNameFor('baz-collection'); | ||
// wait for the load to occur | ||
await promisedSleep(50); | ||
expect(mockSearchService.searchCallCount).to.equal(1); | ||
expect(mockLocalCache.storage['collection-name-cache']['baz-collection'].name).to.equal('Baz Collection'); | ||
}); | ||
}); | ||
//# sourceMappingURL=collection-name-cache.test.js.map |
@@ -10,3 +10,3 @@ { | ||
"author": "Internet Archive", | ||
"version": "0.0.1-alpha.5", | ||
"version": "0.0.1-alpha.6", | ||
"main": "dist/index.js", | ||
@@ -13,0 +13,0 @@ "module": "dist/index.js", |
@@ -54,3 +54,3 @@ /* eslint-disable camelcase */ | ||
private defaultPruningInterval = 1000 * 10; | ||
private defaultPruningInterval = 1000 * 30; | ||
@@ -114,2 +114,4 @@ private fetchTimeout: number | null = null; | ||
private maxCacheSize = 2500; | ||
private cacheLoaded = false; | ||
@@ -119,2 +121,3 @@ | ||
searchService: SearchServiceInterface; | ||
maxCacheSize?: number; | ||
localCache?: LocalCacheInterface; | ||
@@ -129,5 +132,6 @@ loadDelay?: number; | ||
this.pruningAge = options.pruningAge ?? this.pruningAge; | ||
this.maxCacheSize = options.maxCacheSize ?? this.maxCacheSize; | ||
this.pruneCache(); | ||
setInterval(async () => { | ||
await this.loadFromCache(); | ||
await this.pruneCache(); | ||
@@ -215,23 +219,46 @@ }, options.pruneInterval ?? this.defaultPruningInterval); | ||
await this.localCache?.set({ | ||
key: this.cacheKeyName, | ||
value: this.collectionNameCache, | ||
ttl: this.cacheTtl, | ||
}); | ||
await this.persistCache(); | ||
} | ||
// prune entries from the cache | ||
async pruneCache(): Promise<void> { | ||
// prune old entries from the cache | ||
await this.loadFromCache(); | ||
const now = Date.now(); | ||
for (const [identifier, storageInfo] of Object.entries( | ||
this.collectionNameCache | ||
)) { | ||
// sorting the keys by lastAccess ascending so we can remove the oldest | ||
const sortedCache = Object.entries(this.collectionNameCache).sort( | ||
(a, b) => { | ||
const aLastAccess = a[1]?.lastAccess ?? 0; | ||
const bLastAccess = b[1]?.lastAccess ?? 0; | ||
return aLastAccess - bLastAccess; | ||
} | ||
); | ||
const identifiersToDelete = new Set<string>(); | ||
for (const [identifier, storageInfo] of sortedCache) { | ||
if (!storageInfo) continue; | ||
const { lastAccess } = storageInfo; | ||
if (lastAccess < now - this.pruningAge) { | ||
delete this.collectionNameCache[identifier]; | ||
identifiersToDelete.add(identifier); | ||
} | ||
} | ||
// delete oldest identifiers if number is greater than maxCacheSize | ||
if (sortedCache.length > this.maxCacheSize) { | ||
for (let i = 0; i < sortedCache.length - this.maxCacheSize; i += 1) { | ||
const [identifier] = sortedCache[i]; | ||
identifiersToDelete.add(identifier); | ||
} | ||
} | ||
// delete the identifiers from the cache | ||
for (const identifier of identifiersToDelete) { | ||
delete this.collectionNameCache[identifier]; | ||
} | ||
await this.persistCache(); | ||
} | ||
private async persistCache(): Promise<void> { | ||
await this.localCache?.set({ | ||
@@ -238,0 +265,0 @@ key: this.cacheKeyName, |
import { expect } from '@open-wc/testing'; | ||
import { CollectionNameCache } from '../src/collection-name-cache'; | ||
import { MockLocalCache } from './mocks/mock-local-cache'; | ||
import { | ||
@@ -213,3 +214,3 @@ mockSearchResponse, | ||
pruneInterval: 20, | ||
pruningAge: 75, | ||
pruningAge: 80, | ||
}); | ||
@@ -240,2 +241,104 @@ | ||
}); | ||
it('removes old items if caches gets too big', async () => { | ||
const mockSearchService = new MockSearchService(); | ||
mockSearchService.searchResult = mockSearchResponse; | ||
const collectionNameFetcher = new CollectionNameCache({ | ||
searchService: mockSearchService, | ||
loadDelay: 110, | ||
pruneInterval: 150, | ||
maxCacheSize: 2, | ||
}); | ||
// add some time in-between so the timestamps aren't all identical | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
await promisedSleep(50); | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
await promisedSleep(50); | ||
await collectionNameFetcher.collectionNameFor('baz-collection'); | ||
expect(mockSearchService.searchCallCount).to.equal(1); | ||
// waiting 60ms for the pruner to come through and prune the cache, which should remove the first item | ||
// since our max size is 2 | ||
await promisedSleep(60); | ||
// first check the bar-collection, which should not have been pruned so we still only have 1 request | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
expect(mockSearchService.searchCallCount).to.equal(1); | ||
// now we're going to fetch the one that should have been pruned so we should see another request | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
// wait to make sure the load delay elapses | ||
await promisedSleep(120); | ||
// and another request had to be made | ||
expect(mockSearchService.searchCallCount).to.equal(2); | ||
}); | ||
it('can persist the cache to localCache', async () => { | ||
const mockLocalCache = new MockLocalCache(); | ||
const mockSearchService = new MockSearchService(); | ||
mockSearchService.searchResult = mockSearchResponse; | ||
const collectionNameFetcher = new CollectionNameCache({ | ||
searchService: mockSearchService, | ||
localCache: mockLocalCache, | ||
loadDelay: 25, | ||
}); | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
await collectionNameFetcher.collectionNameFor('baz-collection'); | ||
// wait for the load to occur | ||
await promisedSleep(50); | ||
expect( | ||
mockLocalCache.storage['collection-name-cache']['bar-collection'].name | ||
).to.equal('Bar Collection'); | ||
expect( | ||
mockLocalCache.storage['collection-name-cache']['foo-collection'].name | ||
).to.equal('Foo Collection'); | ||
expect( | ||
mockLocalCache.storage['collection-name-cache']['baz-collection'].name | ||
).to.equal('Baz Collection'); | ||
}); | ||
it('will use localCache data if available', async () => { | ||
const mockLocalCache = new MockLocalCache(); | ||
mockLocalCache.storage['collection-name-cache'] = { | ||
'foo-collection': { | ||
name: 'Foo Collection', | ||
timestamp: Date.now(), | ||
}, | ||
'bar-collection': { | ||
name: 'Bar Collection', | ||
lastAccess: Date.now(), | ||
}, | ||
}; | ||
const mockSearchService = new MockSearchService(); | ||
mockSearchService.searchResult = mockSearchResponse; | ||
const collectionNameFetcher = new CollectionNameCache({ | ||
searchService: mockSearchService, | ||
localCache: mockLocalCache, | ||
loadDelay: 25, | ||
}); | ||
await collectionNameFetcher.collectionNameFor('foo-collection'); | ||
await collectionNameFetcher.collectionNameFor('bar-collection'); | ||
await promisedSleep(50); | ||
expect(mockSearchService.searchCallCount).to.equal(0); | ||
// this is not in the cache | ||
await collectionNameFetcher.collectionNameFor('baz-collection'); | ||
// wait for the load to occur | ||
await promisedSleep(50); | ||
expect(mockSearchService.searchCallCount).to.equal(1); | ||
expect( | ||
mockLocalCache.storage['collection-name-cache']['baz-collection'].name | ||
).to.equal('Baz Collection'); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
148920
46
1529