From a1caf2b16a94c388a40bedd8aa1b275a64bb19d3 Mon Sep 17 00:00:00 2001 From: David Evans Date: Wed, 2 May 2018 22:06:16 +0100 Subject: [PATCH] Add cache control headers to server --- bin/handlers/RenderRequestHandler.js | 81 ++++++++++++++++++++++++++++ bin/handlers/render.js | 78 --------------------------- bin/server.js | 10 +++- bin/server/RequestHandler.js | 26 ++++++++- bin/server/StaticRequestHandler.js | 1 + 5 files changed, 116 insertions(+), 80 deletions(-) create mode 100644 bin/handlers/RenderRequestHandler.js delete mode 100644 bin/handlers/render.js diff --git a/bin/handlers/RenderRequestHandler.js b/bin/handlers/RenderRequestHandler.js new file mode 100644 index 0000000..a802572 --- /dev/null +++ b/bin/handlers/RenderRequestHandler.js @@ -0,0 +1,81 @@ +const {HttpError} = require('../server/HttpError'); +const {RequestHandler} = require('../server/RequestHandler'); +const {VirtualSequenceDiagram} = require('../../lib/sequence-diagram'); + +const NUM_MATCH = '[0-9]+(?:\\.[0-9]+)?'; + +function beginTimer() { + return process.hrtime(); +} + +function endTimer(timer) { + const delay = process.hrtime(timer); + return delay[0] * 1e9 + delay[1]; +} + +function getNumeric(v, name) { + if(!v) { + return null; + } + const n = Number.parseFloat(v); + if(Number.isNaN(n)) { + throw new HttpError(400, 'Invalid value for ' + name); + } + return n; +} + +function readEncoded(str, encoding) { + switch(encoding) { + case 'b64': + return Buffer + .from(decodeURIComponent(str), 'base64') + .toString('utf8'); + case 'uri': + return str.split('/').map(decodeURIComponent).join('\n'); + default: + throw new HttpError(400, 'Unknown encoding'); + } +} + +class RenderRequestHandler extends RequestHandler { + constructor(baseUrlPattern) { + super('GET', new RegExp( + `^${baseUrlPattern}/` + + `(?:(?:w(${NUM_MATCH}))?(?:h(${NUM_MATCH}))?/|z(${NUM_MATCH})/)?` + + '(?:(uri|b64)/)?' + + '(.*?)' + + '(?:\\.(svg))?$', + 'i' + )); + this.info = `Rendering sequence diagrams at ${baseUrlPattern}/`; + } + + handle(req, res, {match, pickEncoding, log, writeEncoded}) { + this.applyCommonHeaders(req, res); + + const encoding = pickEncoding(); + const size = { + height: getNumeric(match[2], 'height'), + width: getNumeric(match[1], 'width'), + zoom: getNumeric(match[3], 'zoom'), + }; + const code = readEncoded(match[5], (match[4] || 'uri').toLowerCase()); + const format = (match[6] || 'svg').toLowerCase(); + + const timer = beginTimer(); + const svg = VirtualSequenceDiagram.render(code, {size}); + const delay = endTimer(timer); + log('RENDER (' + (delay / 1e6).toFixed(3) + 'ms)'); + + switch(format) { + case 'svg': + res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8'); + writeEncoded(encoding, svg); + break; + default: + throw new HttpError(400, 'Unsupported image format'); + } + } +} + +module.exports = {RenderRequestHandler}; diff --git a/bin/handlers/render.js b/bin/handlers/render.js deleted file mode 100644 index aea5b4a..0000000 --- a/bin/handlers/render.js +++ /dev/null @@ -1,78 +0,0 @@ -const {HttpError} = require('../server/HttpError'); -const {RequestHandler} = require('../server/RequestHandler'); -const {VirtualSequenceDiagram} = require('../../lib/sequence-diagram'); - -function beginTimer() { - return process.hrtime(); -} - -function endTimer(timer) { - const delay = process.hrtime(timer); - return delay[0] * 1e9 + delay[1]; -} - -const NUM_MATCH = '[0-9]+(?:\\.[0-9]+)?'; -const MATCH_RENDER = new RegExp( - '^/render/' + - `(?:(?:w(${NUM_MATCH}))?(?:h(${NUM_MATCH}))?/|z(${NUM_MATCH})/)?` + - '(?:(uri|b64)/)?' + - '(.*?)' + - '(?:\\.(svg))?$', - 'i' -); - -function getNumeric(v, name) { - if(!v) { - return null; - } - const n = Number.parseFloat(v); - if(Number.isNaN(n)) { - throw new HttpError(400, 'Invalid value for ' + name); - } - return n; -} - -function readEncoded(str, encoding) { - switch(encoding) { - case 'b64': - return Buffer - .from(decodeURIComponent(str), 'base64') - .toString('utf8'); - case 'uri': - return str.split('/').map(decodeURIComponent).join('\n'); - default: - throw new HttpError(400, 'Unknown encoding'); - } -} - -function handleRender(req, res, {match, pickEncoding, log, writeEncoded}) { - res.setHeader('Access-Control-Allow-Origin', '*'); - - const encoding = pickEncoding(); - const size = { - height: getNumeric(match[2], 'height'), - width: getNumeric(match[1], 'width'), - zoom: getNumeric(match[3], 'zoom'), - }; - const code = readEncoded(match[5], (match[4] || 'uri').toLowerCase()); - const format = (match[6] || 'svg').toLowerCase(); - - const timer = beginTimer(); - const svg = VirtualSequenceDiagram.render(code, {size}); - const delay = endTimer(timer); - log('RENDER (' + (delay / 1e6).toFixed(3) + 'ms)'); - - switch(format) { - case 'svg': - res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8'); - writeEncoded(encoding, svg); - break; - default: - throw new HttpError(400, 'Unsupported image format'); - } -} - -const render = new RequestHandler('GET', MATCH_RENDER, handleRender); -render.info = 'Rendering sequence diagrams at /render/'; - -module.exports = {render}; diff --git a/bin/server.js b/bin/server.js index 5666f01..350e1fc 100755 --- a/bin/server.js +++ b/bin/server.js @@ -2,7 +2,7 @@ const {Server} = require('./server/Server'); const {StaticRequestHandler} = require('./server/StaticRequestHandler'); -const {render} = require('./handlers/render'); +const {RenderRequestHandler} = require('./handlers/RenderRequestHandler'); const path = require('path'); const DEV = process.argv.includes('dev'); @@ -31,7 +31,11 @@ function devMapper(file, type, data) { } } +const STATIC_MAX_AGE = 10 * 60; // 10 minutes +const RENDER_MAX_AGE = 60 * 60 * 24 * 7; // 1 week + const statics = new StaticRequestHandler('') + .setCacheMaxAge(DEV ? 0 : STATIC_MAX_AGE) .addMimeType('txt', 'text/plain; charset=utf-8') .addMimeType('htm', 'text/html; charset=utf-8') .addMimeType('html', 'text/html; charset=utf-8') @@ -62,6 +66,10 @@ if(DEV) { statics.setFileWatch(true); } +const render = new RenderRequestHandler('/render') + .setCacheMaxAge(DEV ? 0 : RENDER_MAX_AGE) + .setCrossOrigin(true); + new Server() .addHandler(render) .addHandler(statics) diff --git a/bin/server/RequestHandler.js b/bin/server/RequestHandler.js index 8548de6..0845e6a 100644 --- a/bin/server/RequestHandler.js +++ b/bin/server/RequestHandler.js @@ -3,7 +3,31 @@ class RequestHandler { this.method = method; this.matcher = matcher; this.handleFn = handleFn; - this.info = 'Custom handler at ' + this.method + ' ' + this.matcher; + this.cacheMaxAge = 0; + this.allowAllOrigins = false; + this.info = `Custom handler at ${this.method} ${this.matcher}`; + } + + setCacheMaxAge(seconds) { + this.cacheMaxAge = seconds; + return this; + } + + setCrossOrigin(allowAll) { + this.allowAllOrigins = allowAll; + return this; + } + + applyCommonHeaders(req, res) { + if(this.allowAllOrigins) { + res.setHeader('Access-Control-Allow-Origin', '*'); + } + if(this.cacheMaxAge > 0) { + res.setHeader( + 'Cache-Control', + `public, max-age=${this.cacheMaxAge}` + ); + } } apply(req, res, info) { diff --git a/bin/server/StaticRequestHandler.js b/bin/server/StaticRequestHandler.js index f2e1369..cdb5ab2 100644 --- a/bin/server/StaticRequestHandler.js +++ b/bin/server/StaticRequestHandler.js @@ -51,6 +51,7 @@ class StaticRequestHandler extends RequestHandler { _handleResource(req, res, resource, {pickEncoding, log}) { log('SERVE ' + resource.path); + this.applyCommonHeaders(req, res); const encoding = pickEncoding( PREF_ENCODINGS.filter((enc) => (resource.encodings[enc] !== null)) );