Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
24 changes: 22 additions & 2 deletions docs/docs/api/CacheStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,28 @@ The `MemoryCacheStore` stores the responses in-memory.

**Options**

- `maxSize` - The maximum total size in bytes of all stored responses. Default `Infinity`.
- `maxCount` - The maximum amount of responses to store. Default `Infinity`.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.

### Getters

#### `MemoryCacheStore.size`

Returns the current total size in bytes of all stored responses.

### Methods

#### `MemoryCacheStore.isFull()`

Returns a boolean indicating whether the cache has reached its maximum size or count.

### Events

#### `'maxSizeExceeded'`

Emitted when the cache exceeds its maximum size or count limits. The event payload contains `size`, `maxSize`, `count`, and `maxCount` properties.


### `SqliteCacheStore`

Expand All @@ -26,7 +46,7 @@ The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present.

- `location` - The location of the SQLite database to use. Default `:memory:`.
- `maxCount` - The maximum number of entries to store in the database. Default `Infinity`.
- `maxEntrySize` - The maximum size in bytes that a resposne's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.

## Defining a Custom Cache Store

Expand Down
40 changes: 39 additions & 1 deletion lib/cache/memory-cache-store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const { Writable } = require('node:stream')
const { EventEmitter } = require('node:events')
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')

/**
Expand All @@ -12,20 +13,23 @@ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')

/**
* @implements {CacheStore}
* @extends {EventEmitter}
*/
class MemoryCacheStore {
class MemoryCacheStore extends EventEmitter {
#maxCount = Infinity
#maxSize = Infinity
#maxEntrySize = Infinity

#size = 0
#count = 0
#entries = new Map()
#hasEmittedMaxSizeEvent = false

/**
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
*/
constructor (opts) {
super()
if (opts) {
if (typeof opts !== 'object') {
throw new TypeError('MemoryCacheStore options must be an object')
Expand Down Expand Up @@ -66,6 +70,22 @@ class MemoryCacheStore {
}
}

/**
* Get the current size of the cache in bytes
* @returns {number} The current size of the cache in bytes
*/
get size () {
return this.#size
}

/**
* Check if the cache is full (either max size or max count reached)
* @returns {boolean} True if the cache is full, false otherwise
*/
isFull () {
return this.#size >= this.#maxSize || this.#count >= this.#maxCount
}

/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
Expand Down Expand Up @@ -144,7 +164,20 @@ class MemoryCacheStore {

store.#size += entry.size

// Check if cache is full and emit event if needed
if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
// Emit maxSizeExceeded event if we haven't already
if (!store.#hasEmittedMaxSizeEvent) {
store.emit('maxSizeExceeded', {
size: store.#size,
maxSize: store.#maxSize,
count: store.#count,
maxCount: store.#maxCount
})
store.#hasEmittedMaxSizeEvent = true
}

// Perform eviction
for (const [key, entries] of store.#entries) {
for (const entry of entries.splice(0, entries.length / 2)) {
store.#size -= entry.size
Expand All @@ -154,6 +187,11 @@ class MemoryCacheStore {
store.#entries.delete(key)
}
}

// Reset the event flag after eviction
if (store.#size < store.#maxSize && store.#count < store.#maxCount) {
store.#hasEmittedMaxSizeEvent = false
}
}

callback(null)
Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/wpt/xhr/formdata/append.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
assert_equals(create_formdata(['key', null], ['key', 'value1']).get('key'), "null");
}, 'testFormDataAppendNull2');
test(function() {
var before = new Date(new Date().getTime() - 2000); // two seconds ago, in case there's clock drift
var before = Date.now() - 2000; // Two seconds ago,(in case there's clock drift) using timestamp number
var fd = create_formdata(['key', new Blob(), 'blank.txt']).get('key');
assert_equals(fd.name, "blank.txt");
assert_equals(fd.type, "");
assert_equals(fd.size, 0);
assert_greater_than_equal(fd.lastModified, before);
assert_less_than_equal(fd.lastModified, new Date());
assert_less_than_equal(fd.lastModified, Date.now());
}, 'testFormDataAppendEmptyBlob');

function create_formdata() {
Expand Down
123 changes: 123 additions & 0 deletions test/memory-cache-store-size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
'use strict'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please move this test together to the others of the cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I moved them to memory-cache-store-tests.js. Let me know if you had somewhere else in mind.


const { describe, test } = require('node:test')
const { equal } = require('node:assert')
const MemoryCacheStore = require('../lib/cache/memory-cache-store')

describe('Cache Store', () => {
test('size getter returns correct total size', async () => {
const store = new MemoryCacheStore()
const testData = 'test data'

equal(store.size, 0, 'Initial size should be 0')

const writeStream = store.createWriteStream(
{ origin: 'test', path: '/', method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)

writeStream.write(testData)
writeStream.end()

equal(store.size, testData.length, 'Size should match written data length')
})

test('isFull returns false when under limits', () => {
const store = new MemoryCacheStore({
maxSize: 1000,
maxCount: 10
})

equal(store.isFull(), false, 'Should not be full when empty')
})

test('isFull returns true when maxSize reached', async () => {
const maxSize = 10
const store = new MemoryCacheStore({ maxSize })
const testData = 'x'.repeat(maxSize + 1) // Exceed maxSize

const writeStream = store.createWriteStream(
{ origin: 'test', path: '/', method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)

writeStream.write(testData)
writeStream.end()

equal(store.isFull(), true, 'Should be full when maxSize exceeded')
})

test('isFull returns true when maxCount reached', async () => {
const maxCount = 2
const store = new MemoryCacheStore({ maxCount })

// Add maxCount + 1 entries
for (let i = 0; i <= maxCount; i++) {
const writeStream = store.createWriteStream(
{ origin: 'test', path: `/${i}`, method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)
writeStream.end('test')
}

equal(store.isFull(), true, 'Should be full when maxCount exceeded')
})

test('emits maxSizeExceeded event when limits exceeded', async () => {
const maxSize = 10
const store = new MemoryCacheStore({ maxSize })

let eventFired = false
let eventPayload = null

store.on('maxSizeExceeded', (payload) => {
eventFired = true
eventPayload = payload
})

const testData = 'x'.repeat(maxSize + 1) // Exceed maxSize

const writeStream = store.createWriteStream(
{ origin: 'test', path: '/', method: 'GET' },
{
statusCode: 200,
statusMessage: 'OK',
headers: {},
cachedAt: Date.now(),
staleAt: Date.now() + 1000,
deleteAt: Date.now() + 2000
}
)

writeStream.write(testData)
writeStream.end()

equal(eventFired, true, 'maxSizeExceeded event should fire')
equal(typeof eventPayload, 'object', 'Event should have payload')
equal(typeof eventPayload.size, 'number', 'Payload should have size')
equal(typeof eventPayload.maxSize, 'number', 'Payload should have maxSize')
equal(typeof eventPayload.count, 'number', 'Payload should have count')
equal(typeof eventPayload.maxCount, 'number', 'Payload should have maxCount')
})
})
Loading