Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c6be0eb
Bring in search logic
thehabes Oct 15, 2025
a11c7e6
It searches!
thehabes Oct 15, 2025
4cefb0b
idNegotiation on search results
thehabes Oct 15, 2025
5ac7e49
All the search logic
thehabes Oct 15, 2025
e0f5b00
Lint, and add support for passing search options into the endpoint
thehabes Oct 15, 2025
c148362
polish
thehabes Oct 15, 2025
e3da99e
Update API documentation
thehabes Oct 15, 2025
9bb1234
polish
thehabes Oct 15, 2025
8d38409
polish
thehabes Oct 15, 2025
c376bd4
polish
thehabes Oct 15, 2025
e3f0553
polish
thehabes Oct 15, 2025
77d5430
polish
thehabes Oct 15, 2025
ab2505e
polish
thehabes Oct 15, 2025
d3386eb
polish
thehabes Oct 15, 2025
cadc2f1
polish
thehabes Oct 15, 2025
efb7dbf
exists test for new routes
thehabes Oct 15, 2025
e5f2486
Update public/API.html
thehabes Oct 15, 2025
052be8c
Update public/API.html
thehabes Oct 15, 2025
618a3f3
Update public/API.html
thehabes Oct 15, 2025
4a0093d
Update controllers/search.js
thehabes Oct 15, 2025
477abfa
Update controllers/search.js
thehabes Oct 15, 2025
f1b79f7
get rid of utils. prefix from createExpressError
thehabes Oct 15, 2025
6d063b2
Update public/API.html
thehabes Oct 15, 2025
065e6bb
Update public/API.html
thehabes Oct 15, 2025
4108379
slop formatting
thehabes Oct 15, 2025
b45e2fc
Touch ups to API.html as discussed at standup.
thehabes Oct 16, 2025
18afbd4
bump version because of new search feature
thehabes Oct 16, 2025
b28d7bc
initia idea
thehabes Oct 17, 2025
a6e60c3
tests for cache
thehabes Oct 20, 2025
a8d368c
gog routes too
thehabes Oct 20, 2025
0e18316
cleanup
thehabes Oct 20, 2025
970eaed
fix cachiung
thehabes Oct 20, 2025
793fd62
oh baby a lot going on here
thehabes Oct 20, 2025
1e52989
merge in main
thehabes Oct 20, 2025
9016fd8
structure
thehabes Oct 20, 2025
84158db
Update cache/__tests__/cache.test.js
thehabes Oct 20, 2025
24cf701
Changes from testing
thehabes Oct 21, 2025
c05d4d5
Changes from testing
thehabes Oct 21, 2025
15370ec
Changes from testing
thehabes Oct 21, 2025
f0d31ba
changes from testing
thehabes Oct 21, 2025
ec744af
Update docs for limit control
thehabes Oct 21, 2025
0deea37
update tests
thehabes Oct 21, 2025
1f3fc8c
changes from testing
thehabes Oct 21, 2025
856cd1c
changes from testing
thehabes Oct 21, 2025
ebd9b04
Move test files
thehabes Oct 21, 2025
6cf9e21
documentation
thehabes Oct 21, 2025
05bf04c
cleanup
thehabes Oct 21, 2025
4f0ba84
add status
thehabes Oct 21, 2025
dd90275
changes from testing
thehabes Oct 21, 2025
c8e7a45
changes from testing
thehabes Oct 21, 2025
2e39802
remove this from details
thehabes Oct 21, 2025
1c81ebf
reduce logs
thehabes Oct 21, 2025
c4cdcd5
amendments
thehabes Oct 21, 2025
5558b46
updated integration test
thehabes Oct 22, 2025
bcd7829
closer to real stress tests
thehabes Oct 23, 2025
46943e6
Metrics
thehabes Oct 23, 2025
777f9aa
Catch those hits
thehabes Oct 24, 2025
f75d04e
changes from testing scripts in local environment
thehabes Oct 24, 2025
030366a
changes from testing scripts in local environment
thehabes Oct 24, 2025
2973d61
changes from testing scripts in local environment
thehabes Oct 24, 2025
2ba15f8
Changes from testing in local environment
thehabes Oct 24, 2025
ca97954
Changes from testing in local environment
thehabes Oct 24, 2025
4a793be
Changes from testing in local environment
thehabes Oct 24, 2025
b8a70b0
Changes from testing in local environment
thehabes Oct 24, 2025
11d815c
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
e9666c3
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
aa934da
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
1fca678
requirements for running the .sh files in localhost environments
thehabes Oct 24, 2025
20da77d
updates from testing
thehabes Oct 24, 2025
f14072d
updates from testing
thehabes Oct 24, 2025
128c3e7
Changes from running between environments
thehabes Oct 24, 2025
14d25f9
Changes from running between environments
thehabes Oct 24, 2025
d2f6358
Changes from testing across environments
thehabes Oct 24, 2025
1904584
changes from testing across environments
thehabes Oct 24, 2025
ebcc2da
changes from testing across environments
thehabes Oct 24, 2025
7cfed96
Changes from testing across environments
thehabes Oct 24, 2025
02e1a01
Changes from testing across environments
thehabes Oct 24, 2025
0dfedd8
Changes from testing across environments
thehabes Oct 24, 2025
c4373b8
log touchups
thehabes Oct 24, 2025
b8f6b13
This should just be a warning not a failure
thehabes Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
372 changes: 372 additions & 0 deletions cache/__tests__/cache-limits.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
/**
* Cache limit enforcement tests
* Verifies that the cache properly enforces maxLength and maxBytes limits
* @author thehabes
*/

import { jest } from '@jest/globals'
import cache from '../index.js'

/**
* Helper to create a test cache with custom limits
* We'll manipulate the singleton cache's limits for testing
*/
function setupTestCache(maxLength, maxBytes, ttl = 300000) {
cache.clear()
cache.maxLength = maxLength
cache.maxBytes = maxBytes
cache.ttl = ttl
// Reset stats
cache.stats = {
hits: 0,
misses: 0,
evictions: 0,
sets: 0,
invalidations: 0
}
return cache
}

/**
* Helper to restore default cache settings
*/
function restoreDefaultCache() {
cache.clear()
cache.maxLength = parseInt(process.env.CACHE_MAX_LENGTH ?? 1000)
cache.maxBytes = parseInt(process.env.CACHE_MAX_BYTES ?? 1000000000)
cache.ttl = parseInt(process.env.CACHE_TTL ?? 300000)
cache.stats = {
hits: 0,
misses: 0,
evictions: 0,
sets: 0,
invalidations: 0
}
}

describe('Cache Length Limit Enforcement', () => {
let testCache

beforeEach(() => {
testCache = setupTestCache(10, 1000000000, 300000)
})

afterEach(() => {
restoreDefaultCache()
})

it('should not exceed maxLength when adding entries', () => {
const maxLength = 10

// Add more entries than the limit
for (let i = 0; i < 20; i++) {
const key = testCache.generateKey('id', `test${i}`)
testCache.set(key, { data: `value${i}` })
}

// Cache should never exceed maxLength
expect(testCache.cache.size).toBeLessThanOrEqual(maxLength)
expect(testCache.cache.size).toBe(maxLength)

// Should have evicted the oldest entries
expect(testCache.stats.evictions).toBe(10)
})

it('should evict least recently used entries when limit is reached', () => {
testCache = setupTestCache(5, 1000000000, 300000)

// Add 5 entries
for (let i = 0; i < 5; i++) {
const key = testCache.generateKey('id', `test${i}`)
testCache.set(key, { data: `value${i}` })
}

expect(testCache.cache.size).toBe(5)

// Add one more entry, should evict test0
const key6 = testCache.generateKey('id', 'test5')
testCache.set(key6, { data: 'value5' })

expect(testCache.cache.size).toBe(5)

// test0 should be evicted (it was the first, least recently used)
const key0 = testCache.generateKey('id', 'test0')
const result = testCache.get(key0)
expect(result).toBeNull()

// test5 should be present
const result5 = testCache.get(key6)
expect(result5).toEqual({ data: 'value5' })
})

it('should maintain LRU order when accessing entries', () => {
testCache = setupTestCache(3, 1000000000, 300000)

// Add 3 entries
const key1 = testCache.generateKey('id', 'test1')
const key2 = testCache.generateKey('id', 'test2')
const key3 = testCache.generateKey('id', 'test3')

testCache.set(key1, { data: 'value1' })
testCache.set(key2, { data: 'value2' })
testCache.set(key3, { data: 'value3' })

// Access test1 to make it most recently used
testCache.get(key1)

// Add a new entry, should evict test2 (oldest)
const key4 = testCache.generateKey('id', 'test4')
testCache.set(key4, { data: 'value4' })

// test2 should be evicted
expect(testCache.get(key2)).toBeNull()

// test1 should still be present (was accessed recently)
expect(testCache.get(key1)).toEqual({ data: 'value1' })

// test3 and test4 should be present
expect(testCache.get(key3)).toEqual({ data: 'value3' })
expect(testCache.get(key4)).toEqual({ data: 'value4' })
})
})

describe('Cache Size (Bytes) Limit Enforcement', () => {
let testCache

beforeEach(() => {
testCache = setupTestCache(1000, 500, 300000) // 500 bytes limit
})

afterEach(() => {
restoreDefaultCache()
})

it('should not exceed maxBytes when adding entries', () => {
// Create entries with known size
// Each entry will be roughly 50-60 bytes when serialized
const largeValue = { data: 'x'.repeat(50) }

// Add entries until we exceed the byte limit
for (let i = 0; i < 20; i++) {
const key = testCache.generateKey('id', `test${i}`)
testCache.set(key, largeValue)
}

// Cache should never exceed maxBytes
const currentBytes = Buffer.byteLength(JSON.stringify(testCache.cache), 'utf8')
expect(currentBytes).toBeLessThanOrEqual(500)

// Should have evicted some entries
expect(testCache.stats.evictions).toBeGreaterThan(0)
})

it('should evict multiple entries if needed to stay under byte limit', () => {
testCache = setupTestCache(1000, 200, 300000) // Very small limit

// Add a few small entries
for (let i = 0; i < 3; i++) {
const key = testCache.generateKey('id', `small${i}`)
testCache.set(key, { data: 'tiny' })
}

const initialSize = testCache.cache.size
expect(initialSize).toBeGreaterThan(0)

// Add a large entry that will force multiple evictions
const largeKey = testCache.generateKey('id', 'large')
const largeValue = { data: 'x'.repeat(100) }
testCache.set(largeKey, largeValue)

// Should have evicted entries to make room
const currentBytes = Buffer.byteLength(JSON.stringify(testCache.cache), 'utf8')
expect(currentBytes).toBeLessThanOrEqual(200)
})

it('should handle byte limit with realistic cache entries', () => {
testCache = setupTestCache(1000, 5000, 300000) // 5KB limit

// Simulate realistic query cache entries
const sampleQuery = {
type: 'Annotation',
body: {
value: 'Sample annotation text',
format: 'text/plain'
}
}

const sampleResults = Array.from({ length: 10 }, (_, i) => ({
'@id': `http://example.org/annotation/${i}`,
'@type': 'Annotation',
body: {
value: `Annotation content ${i}`,
format: 'text/plain'
},
target: `http://example.org/target/${i}`
}))

// Add multiple query results
for (let i = 0; i < 10; i++) {
const key = testCache.generateKey('query', { ...sampleQuery, page: i })
testCache.set(key, sampleResults)
}

// Verify byte limit is enforced
const currentBytes = Buffer.byteLength(JSON.stringify(testCache.cache), 'utf8')
expect(currentBytes).toBeLessThanOrEqual(5000)

// Should have some entries cached
expect(testCache.cache.size).toBeGreaterThan(0)
})
})

describe('Combined Length and Size Limits', () => {
let testCache

beforeEach(() => {
testCache = setupTestCache(10, 2000, 300000)
})

afterEach(() => {
restoreDefaultCache()
})

it('should enforce both length and byte limits', () => {
// Add entries with varying sizes
for (let i = 0; i < 20; i++) {
const key = testCache.generateKey('id', `test${i}`)
const size = i * 10 // Varying sizes
testCache.set(key, { data: 'x'.repeat(size) })
}

// Should respect both limits
expect(testCache.cache.size).toBeLessThanOrEqual(10)

const currentBytes = Buffer.byteLength(JSON.stringify(testCache.cache), 'utf8')
expect(currentBytes).toBeLessThanOrEqual(2000)
})

it('should prioritize byte limit over length limit when necessary', () => {
testCache = setupTestCache(100, 500, 300000) // High length limit, low byte limit

// Add large entries that will hit byte limit before length limit
const largeValue = { data: 'x'.repeat(50) }

for (let i = 0; i < 20; i++) {
const key = testCache.generateKey('id', `test${i}`)
testCache.set(key, largeValue)
}

// Should have fewer entries than maxLength due to byte limit
expect(testCache.cache.size).toBeLessThan(100)
expect(testCache.cache.size).toBeGreaterThan(0)

// Should respect byte limit
const currentBytes = Buffer.byteLength(JSON.stringify(testCache.cache), 'utf8')
expect(currentBytes).toBeLessThanOrEqual(500)
})
})

describe('Edge Cases', () => {
let testCache

beforeEach(() => {
testCache = setupTestCache(5, 1000000000, 300000)
})

afterEach(() => {
restoreDefaultCache()
})

it('should handle updating existing entries without exceeding limits', () => {
// Fill cache to limit
for (let i = 0; i < 5; i++) {
const key = testCache.generateKey('id', `test${i}`)
testCache.set(key, { data: `value${i}` })
}

expect(testCache.cache.size).toBe(5)

// Update an existing entry (should not trigger eviction)
const key2 = testCache.generateKey('id', 'test2')
testCache.set(key2, { data: 'updated value' })

expect(testCache.cache.size).toBe(5)
expect(testCache.get(key2)).toEqual({ data: 'updated value' })
})

it('should handle single large entry that fits within limits', () => {
testCache = setupTestCache(1000, 1000, 300000)

// Add a large but valid entry
const largeKey = testCache.generateKey('id', 'large')
const largeValue = { data: 'x'.repeat(200) }
testCache.set(largeKey, largeValue)

expect(testCache.cache.size).toBe(1)
expect(testCache.get(largeKey)).toEqual(largeValue)
})

it('should handle empty cache when checking limits', () => {
testCache = setupTestCache(10, 1000, 300000)

expect(testCache.cache.size).toBe(0)

const stats = testCache.getStats()
expect(stats.length).toBe(0)
expect(stats.maxLength).toBe(10)
expect(stats.maxBytes).toBe(1000)
})
})

describe('Real-world Simulation', () => {
let testCache

beforeEach(() => {
// Use actual default values from production
testCache = setupTestCache(1000, 1000000000, 300000)
})

afterEach(() => {
restoreDefaultCache()
})

it('should handle realistic RERUM API cache usage', () => {
// Simulate 2000 cache operations (should trigger evictions)
for (let i = 0; i < 2000; i++) {
const key = testCache.generateKey('query', {
type: 'Annotation',
'@context': 'http://www.w3.org/ns/anno.jsonld',
page: Math.floor(i / 10)
})

// Realistic result set
const results = Array.from({ length: 100 }, (_, j) => ({
'@id': `http://store.rerum.io/v1/id/${i}_${j}`,
'@type': 'Annotation'
}))

testCache.set(key, results)
}

// Should respect length limit
expect(testCache.cache.size).toBeLessThanOrEqual(1000)

// Due to the page grouping (Math.floor(i/10)), we actually only have 200 unique keys
// (2000 / 10 = 200 unique page numbers)
// So the final cache size should be 200, not 1000
expect(testCache.cache.size).toBe(200)

// No evictions should occur because we only created 200 unique entries
// (Each i/10 page gets overwritten 10 times, not added)
expect(testCache.stats.evictions).toBe(0)

// Stats should show 2000 sets (including overwrites)
const stats = testCache.getStats()
expect(stats.sets).toBe(2000)
expect(stats.length).toBe(200)

// Verify byte limit is not exceeded
expect(stats.bytes).toBeLessThanOrEqual(1000000000)
})
})

Loading