From 3568d391782fc67021be692372f5c5d376128681 Mon Sep 17 00:00:00 2001 From: Lars Trieloff Date: Wed, 11 Mar 2020 14:27:07 +0000 Subject: [PATCH] feat(utils): add cache-utils for merging cache-control headers --- src/html/data-sections.js | 11 ++++- src/utils/cache-helper.js | 52 ++++++++++++++++++++++++ test/testCacheHelper.js | 84 +++++++++++++++++++++++++++++++++++++++ test/testDataEmbeds.js | 11 ++++- 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/utils/cache-helper.js create mode 100644 test/testCacheHelper.js diff --git a/src/html/data-sections.js b/src/html/data-sections.js index eb47be543..3be8cdee2 100644 --- a/src/html/data-sections.js +++ b/src/html/data-sections.js @@ -17,6 +17,7 @@ const { } = require('ferrum'); const removePosition = require('unist-util-remove-position'); const dotprop = require('dot-prop'); +const { merge } = require('../utils/cache-helper'); const pattern = /{{([^{}]+)}}/g; /** @@ -158,9 +159,17 @@ async function fillDataSections(context, { downloader, logger }) { // remember that we are using this source so that we can compute the // surrogate key later - setdefault(context.content, 'sources', []); context.content.sources.push(node.url); + + // pass the cache control header through + const res = setdefault(context, 'response', {}); + const headers = setdefault(res, 'headers', {}); + + headers['Cache-Control'] = merge( + headers['Cache-Control'], + downloadeddata.headers.get('cache-control'), + ); } catch (e) { logger.warn(`Unable to parse JSON for data embed ${node.url}: ${e.message}`); return node; diff --git a/src/utils/cache-helper.js b/src/utils/cache-helper.js new file mode 100644 index 000000000..60d4068ae --- /dev/null +++ b/src/utils/cache-helper.js @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +function directives(expression = '') { + const retval = expression.split(',') + .map((s) => s.trim()) + .filter((s) => !!s) + .map((s) => s.split('=')) + .map(([ + directive, + value]) => [directive, Number.isNaN(Number.parseInt(value, 10)) + ? true + : Number.parseInt(value, 10)]) + .reduce((obj, [directive, value]) => { + obj[directive] = value; + return obj; + }, {}); + return retval; +} + +function format(dirs = {}) { + return Object.entries(dirs) + .map(([directive, value]) => ((value === true || value === 1) ? directive : `${directive}=${value}`)) + .join(', '); +} + +function merge(in1 = '', in2 = '') { + const dirs1 = typeof in1 === 'string' ? directives(in1) : in1; + const dirs2 = typeof in2 === 'string' ? directives(in2) : in2; + + const keys = [...Object.keys(dirs1), ...Object.keys(dirs2)]; + + const mergeval = keys.reduce((merged, key) => { + merged[key] = Math.min( + dirs1[key] || Number.MAX_SAFE_INTEGER, + dirs2[key] || Number.MAX_SAFE_INTEGER, + ); + return merged; + }, {}); + + return typeof in1 === 'string' ? format(mergeval) : mergeval; +} + +module.exports = { directives, format, merge }; diff --git a/test/testCacheHelper.js b/test/testCacheHelper.js new file mode 100644 index 000000000..ae355a87b --- /dev/null +++ b/test/testCacheHelper.js @@ -0,0 +1,84 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-env mocha */ +const assert = require('assert'); +const { directives, format, merge } = require('../src/utils/cache-helper'); + +describe('Cache Helper Tests (surrogate)', () => { + it('directive parses directives', () => { + assert.deepStrictEqual(directives('max-age=300'), { + 'max-age': 300, + }); + + assert.deepStrictEqual(directives('s-maxage=300, max-age=300'), { + 's-maxage': 300, + 'max-age': 300, + }); + + assert.deepStrictEqual(directives('s-maxage=300, max-age=300, public'), { + 's-maxage': 300, + 'max-age': 300, + public: true, + }); + + assert.deepStrictEqual(directives(''), {}); + + assert.deepStrictEqual(directives(), {}); + }); + + it('format formats directives', () => { + assert.equal(format({}), ''); + assert.equal(format(undefined), ''); + + assert.equal(format({ + 'max-age': 300, + public: true, + }), 'max-age=300, public'); + }); + + it('merge merges two directives', () => { + assert.deepEqual(merge({}, {}), {}); + + assert.deepEqual(merge({ + 'max-age': 300, + }, { + 's-maxage': 300, + }), { + 's-maxage': 300, + 'max-age': 300, + }); + + assert.deepEqual(merge({ + 'max-age': 300, + 's-maxage': 600, + }, { + 's-maxage': 300, + 'max-age': 600, + }), { + 's-maxage': 300, + 'max-age': 300, + }); + + assert.deepEqual(merge({ + public: true, + }, { + private: true, + }), { + public: true, + private: true, + }); + + assert.equal(merge('max-age=300, public', 'max-age=600'), 'max-age=300, public'); + + assert.equal(merge(), ''); + }); +}); diff --git a/test/testDataEmbeds.js b/test/testDataEmbeds.js index c563a167c..9c959f789 100644 --- a/test/testDataEmbeds.js +++ b/test/testDataEmbeds.js @@ -104,6 +104,9 @@ describe('Integration Test with Data Embeds', () => { .reply(() => [404]); nock('https://adobeioruntime.net') + .defaultReplyHeaders({ + 'Cache-Control': 'max-age=3600', + }) .get(/.*/) .reply(() => [status, data]); @@ -125,7 +128,11 @@ describe('Integration Test with Data Embeds', () => { const result = await pipe( (mycontext) => { - mycontext.response = { status: 200, body: mycontext.content.document.body.innerHTML }; + if (!mycontext.response) { + mycontext.response = {}; + } + mycontext.response.status = 200; + mycontext.response.body = mycontext.content.document.body.innerHTML; }, context, action, @@ -213,6 +220,8 @@ https://docs.google.com/spreadsheets/d/e/someotheruri/pubhtml assert.equal(res1.response.headers['Surrogate-Key'], 'PbTcuh0tIarmUOZM'); assert.equal(res2.response.headers['Surrogate-Key'], 'IkqgcxcG5+q8/cOT'); + + assert.equal(res1.response.headers['Cache-Control'], 'max-age=3600'); }); it('html.pipe processes data embeds in main document', async () => testEmbeds(