From 70c3361d375c6bf5fe831ce8e944bc7959d4f1a8 Mon Sep 17 00:00:00 2001
From: Felix Ezama-Vaughan <ezamabusiness@gmail.com>
Date: Wed, 14 May 2025 20:18:41 -0600
Subject: [PATCH 1/4] Initial impl

---
 docs/docs/api/CacheStore.md     |  24 ++++++-
 lib/cache/memory-cache-store.js |  40 ++++++++++-
 test/memory-cache-store-size.js | 123 ++++++++++++++++++++++++++++++++
 3 files changed, 184 insertions(+), 3 deletions(-)
 create mode 100644 test/memory-cache-store-size.js

diff --git a/docs/docs/api/CacheStore.md b/docs/docs/api/CacheStore.md
index 7cd19e08786..0f3b3eebc7f 100644
--- a/docs/docs/api/CacheStore.md
+++ b/docs/docs/api/CacheStore.md
@@ -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`
 
@@ -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
 
diff --git a/lib/cache/memory-cache-store.js b/lib/cache/memory-cache-store.js
index 4640092b5f5..2fb3dfabd19 100644
--- a/lib/cache/memory-cache-store.js
+++ b/lib/cache/memory-cache-store.js
@@ -1,6 +1,7 @@
 'use strict'
 
 const { Writable } = require('node:stream')
+const { EventEmitter } = require('node:events')
 const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
 
 /**
@@ -12,8 +13,9 @@ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
 
 /**
  * @implements {CacheStore}
+ * @extends {EventEmitter}
  */
-class MemoryCacheStore {
+class MemoryCacheStore extends EventEmitter {
   #maxCount = Infinity
   #maxSize = Infinity
   #maxEntrySize = Infinity
@@ -21,11 +23,13 @@ class MemoryCacheStore {
   #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')
@@ -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}
@@ -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
@@ -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)
diff --git a/test/memory-cache-store-size.js b/test/memory-cache-store-size.js
new file mode 100644
index 00000000000..c3995dd9709
--- /dev/null
+++ b/test/memory-cache-store-size.js
@@ -0,0 +1,123 @@
+'use strict'
+
+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')
+  })
+})

From 0454a437162654820dcbd6a0ef9495546e5ef664 Mon Sep 17 00:00:00 2001
From: Felix Ezama-Vaughan <ezamabusiness@gmail.com>
Date: Thu, 15 May 2025 23:23:37 -0600
Subject: [PATCH 2/4] modified failing test

---
 test/fixtures/wpt/xhr/formdata/append.any.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/fixtures/wpt/xhr/formdata/append.any.js b/test/fixtures/wpt/xhr/formdata/append.any.js
index fb365618d20..fba71ca86bb 100644
--- a/test/fixtures/wpt/xhr/formdata/append.any.js
+++ b/test/fixtures/wpt/xhr/formdata/append.any.js
@@ -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, 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() {

From 210488166bae8597481fdfe17a01dc173c5e1166 Mon Sep 17 00:00:00 2001
From: Felix Ezama-Vaughan <ezamabusiness@gmail.com>
Date: Thu, 15 May 2025 23:35:23 -0600
Subject: [PATCH 3/4] modified a comment

---
 test/fixtures/wpt/xhr/formdata/append.any.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/fixtures/wpt/xhr/formdata/append.any.js b/test/fixtures/wpt/xhr/formdata/append.any.js
index fba71ca86bb..7c79ac90329 100644
--- a/test/fixtures/wpt/xhr/formdata/append.any.js
+++ b/test/fixtures/wpt/xhr/formdata/append.any.js
@@ -19,7 +19,7 @@
         assert_equals(create_formdata(['key', null], ['key', 'value1']).get('key'), "null");
     }, 'testFormDataAppendNull2');
     test(function() {
-        var before = Date.now() - 2000; // Two seconds ago, using timestamp number
+        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, "");

From 98c7711db836bfe1bfb19b094dd80bc542430764 Mon Sep 17 00:00:00 2001
From: Felix Ezama-Vaughan <ezamabusiness@gmail.com>
Date: Sun, 18 May 2025 19:29:18 -0600
Subject: [PATCH 4/4] moved tests in memory-cache-store-size.js to
 memory-cache-store-tests.js

---
 .../memory-cache-store-tests.js               | 118 +++++++++++++++++
 test/memory-cache-store-size.js               | 123 ------------------
 2 files changed, 118 insertions(+), 123 deletions(-)
 delete mode 100644 test/memory-cache-store-size.js

diff --git a/test/cache-interceptor/memory-cache-store-tests.js b/test/cache-interceptor/memory-cache-store-tests.js
index 3f2a7d89e63..740cbdc31ce 100644
--- a/test/cache-interceptor/memory-cache-store-tests.js
+++ b/test/cache-interceptor/memory-cache-store-tests.js
@@ -1,6 +1,124 @@
 'use strict'
 
+const { test } = require('node:test')
+const { equal } = require('node:assert')
 const MemoryCacheStore = require('../../lib/cache/memory-cache-store')
 const { cacheStoreTests } = require('./cache-store-test-utils.js')
 
 cacheStoreTests(MemoryCacheStore)
+
+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')
+})
diff --git a/test/memory-cache-store-size.js b/test/memory-cache-store-size.js
deleted file mode 100644
index c3995dd9709..00000000000
--- a/test/memory-cache-store-size.js
+++ /dev/null
@@ -1,123 +0,0 @@
-'use strict'
-
-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')
-  })
-})
