Add cache control headers to server

This commit is contained in:
David Evans 2018-05-02 22:06:16 +01:00
parent f3488f631e
commit a1caf2b16a
5 changed files with 116 additions and 80 deletions

View File

@ -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};

View File

@ -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};

View File

@ -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)

View File

@ -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) {

View File

@ -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))
);