Skip to content

Commit

Permalink
feat: make cache size limit configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
stefan-guggisberg committed Feb 28, 2020
1 parent dc77631 commit 7fd632d
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 15 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ $ npm install @adobe/helix-fetch

### Customization

Set cache size limit (Default: 100 \* 1024 \* 1024 bytes, i.e. 100mb):

```javascript
const { fetch, cacheStats } = require('@adobe/helix-fetch').context({
maxCacheSize: 100 * 1024, // 100kb
});

let resp = await fetch('http://httpbin.org/bytes/60000'); // ~60kb response
resp = await fetch('http://httpbin.org/bytes/50000'); // ~50kb response
console.log(cacheStats());
```

Force HTTP/1(.1) protocol:

```javascript
Expand Down
29 changes: 28 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"fetch-h2": "^2.2.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.3",
"lru-cache": "^5.1.1"
"lru-cache": "^5.1.1",
"object-sizeof": "^1.5.3"
},
"devDependencies": {
"@adobe/eslint-config-helix": "1.1.0",
Expand Down
30 changes: 26 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
TimeoutError,
} = require('fetch-h2');
const LRU = require('lru-cache');
const sizeof = require('object-sizeof');

const CachePolicy = require('./policy');
const { cacheableResponse, decoratedResponse } = require('./response');
Expand All @@ -28,7 +29,7 @@ const { decorateHeaders } = require('./headers');
const CACHEABLE_METHODS = ['GET', 'HEAD'];
const DEFAULT_FETCH_OPTIONS = { method: 'GET', cache: 'default' };
const DEFAULT_CONTEXT_OPTIONS = { userAgent: 'helix-fetch', overwriteUserAgent: true };
const DEFAULT_MAX_CACHE_ENTRIES = 100;
const DEFAULT_MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100mb

// events
const PUSH_EVENT = 'push';
Expand Down Expand Up @@ -108,9 +109,14 @@ const wrappedFetch = async (ctx, url, options = {}) => {

class FetchContext {
constructor(options = {}) {
this._ctx = context({ ...DEFAULT_CONTEXT_OPTIONS, ...options });

this._ctx.cache = new LRU({ max: DEFAULT_MAX_CACHE_ENTRIES });
// setup context
const opts = { ...DEFAULT_CONTEXT_OPTIONS, ...options };
this._ctx = context(opts);
// setup cache
const max = typeof opts.maxCacheSize === 'number' && opts.maxCacheSize >= 0 ? opts.maxCacheSize : DEFAULT_MAX_CACHE_SIZE;
const length = ({ response }, _) => sizeof(response);
this._ctx.cache = new LRU({ max, length });
// event emitter
this._ctx.eventEmitter = new EventEmitter();
// register push handler
this._ctx.onPush(createPushHandler(this._ctx));
Expand Down Expand Up @@ -159,7 +165,16 @@ class FetchContext {
*/
offPush: (fn) => this.offPush(fn),

/**
* Clear the cache entirely, throwing away all values.
*/
clearCache: () => this.clearCache(),

/**
* Cache stats for diagnostic purposes
*/
cacheStats: () => this.cacheStats(),

/**
* Error thrown when a request timed out.
*/
Expand Down Expand Up @@ -188,6 +203,13 @@ class FetchContext {
this._ctx.cache.reset();
}

cacheStats() {
return {
size: this._ctx.cache.length,
count: this._ctx.cache.itemCount,
};
}

async fetch(url, options) {
return wrappedFetch(this._ctx, url, options);
}
Expand Down
41 changes: 32 additions & 9 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const parseCacheControl = require('parse-cache-control');
const { WritableStreamBuffer } = require('stream-buffers');

const {
fetch, onPush, offPush, disconnectAll, clearCache, context, TimeoutError,
fetch, onPush, offPush, disconnectAll, clearCache, cacheStats, context, TimeoutError,
} = require('../src/index.js');

const WOKEUP = 'woke up!';
Expand Down Expand Up @@ -59,7 +59,7 @@ describe('Fetch Tests', () => {
});

it('fetch supports binary response body (ArrayBuffer)', async () => {
const dataLen = 64 * 1024; // httpbin.org/stream-bytes has a limit of 100kb ...
const dataLen = 64 * 1024; // httpbin.org/stream-bytes/{n} has a limit of 100kb ...
const contentType = 'application/octet-stream';
const resp = await fetch(`https://httpbin.org/stream-bytes/${dataLen}`, {
headers: { accept: contentType },
Expand All @@ -72,7 +72,7 @@ describe('Fetch Tests', () => {
});

it('fetch supports binary response body (Stream)', async () => {
const dataLen = 64 * 1024; // httpbin.org/stream-bytes has a limit of 100kb ...
const dataLen = 64 * 1024; // httpbin.org/stream-bytes/{n} has a limit of 100kb ...
const contentType = 'application/octet-stream';
const resp = await fetch(`https://httpbin.org/stream-bytes/${dataLen}`, {
headers: { accept: contentType },
Expand Down Expand Up @@ -145,12 +145,35 @@ describe('Fetch Tests', () => {
// clear client cache
clearCache();

const { size, count } = cacheStats();
assert.equal(size, 0);
assert.equal(count, 0);

// re-send request, make sure it's returning a fresh response
resp = await fetch(url);
assert.equal(resp.status, 200);
assert(!resp.fromCache);
});

it('cache size limit is configurable', async () => {
const maxCacheSize = 100 * 1024; // 100kb
// custom context with cache size limit
const ctx = context({ maxCacheSize });

const sizes = [34 * 1024, 35 * 1024, 36 * 1024]; // sizes add up to >100kb
const urls = sizes.map((size) => `http://httpbin.org/bytes/${size}`);
// prime cache with multiple requests that together hit the cache size limit of 100kb
const resps = await Promise.all(urls.map((url) => ctx.fetch(url)));
assert.equal(resps.filter((resp) => resp.status === 200).length, urls.length);

const { size, count } = ctx.cacheStats();
assert(size < maxCacheSize);
assert.equal(count, urls.length - 1);

ctx.clearCache();
await ctx.disconnectAll();
});

// eslint-disable-next-line func-names
it('fetch supports max-age directive', async function () {
this.timeout(5000);
Expand Down Expand Up @@ -388,18 +411,18 @@ describe('Fetch Tests', () => {
});

it('creating custom fetch context works', async () => {
const { fetch: customFetch } = context();
const resp = await customFetch('https://httpbin.org/status/200');
const ctx = context();
const resp = await ctx.fetch('https://httpbin.org/status/200');
assert.equal(resp.status, 200);
});

it('overriding user-agent works', async () => {
const customUserAgent = 'helix-custom-fetch';
const { fetch: customFetch } = context({
const ctx = context({
userAgent: customUserAgent,
overwriteUserAgent: true,
});
const resp = await customFetch('https://httpbin.org/user-agent');
const resp = await ctx.fetch('https://httpbin.org/user-agent');
assert.equal(resp.status, 200);
assert.equal(resp.headers.get('content-type'), 'application/json');
const json = await resp.json();
Expand All @@ -416,10 +439,10 @@ describe('Fetch Tests', () => {
assert.equal(resp.httpVersion, 2);

// custom context forces http1
const { fetch: customFetch } = context({
const ctx = context({
httpsProtocols: ['http1'],
});
resp = await customFetch(url);
resp = await ctx.fetch(url);
assert.equal(resp.status, 200);
assert.equal(resp.httpVersion, 1);
});
Expand Down

0 comments on commit 7fd632d

Please sign in to comment.