mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 13:17:08 +00:00
Add ETag middleware
This commit is contained in:
45
lib/www/server/api/middleware/etag/cache.js
Normal file
45
lib/www/server/api/middleware/etag/cache.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
const cache = {};
|
||||||
|
|
||||||
|
function getCache (r) {
|
||||||
|
if (!r?.app?.locals) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!r.app.locals.etags) {
|
||||||
|
r.app.locals.etags = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.app.locals.etags;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCached (req) {
|
||||||
|
const cache = getCache(req);
|
||||||
|
|
||||||
|
if (req.url in cache) {
|
||||||
|
const cached = cache[req.url];
|
||||||
|
const etag = req.get("If-None-Match");
|
||||||
|
if (etag && etag == cached.etag) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return; // undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveResponse (res) {
|
||||||
|
if (res?.headersSent) {
|
||||||
|
const etag = res.get("ETag");
|
||||||
|
if (etag && res.locals.saveEtag !== false) {
|
||||||
|
const cache = getCache(res);
|
||||||
|
const req = res.req;
|
||||||
|
console.log(`Saving ETag: ${req.method} ${req.url} → ${etag}`);
|
||||||
|
cache[req.url] = {etag, headers: res.getHeaders()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCache,
|
||||||
|
isCached,
|
||||||
|
saveResponse
|
||||||
|
};
|
||||||
39
lib/www/server/api/middleware/etag/if-none-match.js
Normal file
39
lib/www/server/api/middleware/etag/if-none-match.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const { isCached } = require('./cache');
|
||||||
|
|
||||||
|
function isIdempotentMethod (method) {
|
||||||
|
const nonIdempotentMethods = [
|
||||||
|
"POST", "PUT", "PATH", "DELETE"
|
||||||
|
];
|
||||||
|
|
||||||
|
return !nonIdempotentMethods.includes(method.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
function setHeaders(res, headers) {
|
||||||
|
for (let [key, value] of Object.entries(headers)) {
|
||||||
|
res.set(key, value);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ifNoneMatch (req, res, next) {
|
||||||
|
|
||||||
|
const cached = isCached(req);
|
||||||
|
if (cached) {
|
||||||
|
console.log("ETag match", req.url);
|
||||||
|
setHeaders(res, cached.headers);
|
||||||
|
if (req.method == "GET" || req.method == "HEAD") {
|
||||||
|
res.status(304).send();
|
||||||
|
// No next()
|
||||||
|
} else if (!isIdempotentMethod(req.method)) {
|
||||||
|
res.status(412).send();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Either we didn't have this URL in the cache, or there was
|
||||||
|
// no If-None-Match header, or it didn't match the cached ETag.
|
||||||
|
// We let the request proceed normally.
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ifNoneMatch;
|
||||||
7
lib/www/server/api/middleware/etag/index.js
Normal file
7
lib/www/server/api/middleware/etag/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ifNoneMatch: require('./if-none-match'),
|
||||||
|
save: require('./save'),
|
||||||
|
noSave: require('./no-save'),
|
||||||
|
watch: require('./watch')
|
||||||
|
};
|
||||||
7
lib/www/server/api/middleware/etag/no-save.js
Normal file
7
lib/www/server/api/middleware/etag/no-save.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
function noSave (req, res, next) {
|
||||||
|
res.locals.saveEtag = false;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = noSave;
|
||||||
8
lib/www/server/api/middleware/etag/save.js
Normal file
8
lib/www/server/api/middleware/etag/save.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const { saveResponse } = require('./cache');
|
||||||
|
|
||||||
|
function save (req, res, next) {
|
||||||
|
saveResponse(res);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = save;
|
||||||
127
lib/www/server/api/middleware/etag/watch.js
Normal file
127
lib/www/server/api/middleware/etag/watch.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
const { getCache } = require('./cache');
|
||||||
|
const { listen } = require('../../../lib/db/notify');
|
||||||
|
const channels = require('../../../lib/db/channels');
|
||||||
|
|
||||||
|
const rels = [
|
||||||
|
{
|
||||||
|
channels: [ "realtime" ],
|
||||||
|
urls: [ ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "event" ],
|
||||||
|
urls: [ /^\/project\/([^\/]+)\/event/ ],
|
||||||
|
matches: [ "project" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "project" ],
|
||||||
|
urls: [ /^\/project\/([^\/]+)\// ],
|
||||||
|
matches: [ "project" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "preplot_lines", "preplot_points" ],
|
||||||
|
urls: [ /^\/project\/([^\/]+)\/line[\/?]?/ ],
|
||||||
|
matches: [ "project" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "planned_lines" ],
|
||||||
|
urls: [ /^\/project\/([^\/]+)\/plan[\/?]?/ ],
|
||||||
|
matches: [ "project" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "raw_lines", "raw_shots" ],
|
||||||
|
urls: [ /^\/project\/([^\/]+)\/sequence[\/?]?/ ],
|
||||||
|
matches: [ "project" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "final_lines", "final_shots" ],
|
||||||
|
urls: [ /^\/project\/([^\/]+)\/sequence[\/?]?/ ],
|
||||||
|
matches: [ "project" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "info" ],
|
||||||
|
urls: [ ],
|
||||||
|
matches: [ ],
|
||||||
|
callback (url, data) {
|
||||||
|
if (data.payload?.table == "info") {
|
||||||
|
const rx = /^\/project\/([^\/]+)\/info\/([^\/?]+)[\/?]?/;
|
||||||
|
const match = url.match(rx);
|
||||||
|
if (match) {
|
||||||
|
if (match[1] == data.payload.pid) {
|
||||||
|
if (match[2] == data.payload?.old?.key || match[2] == data.payload?.new?.key) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channels: [ "queue_items" ],
|
||||||
|
urls: [ ]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function invalidateCache (data, cache) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const channel = data.channel;
|
||||||
|
const project = data.payload.pid;
|
||||||
|
const operation = data.payload.operation;
|
||||||
|
const table = data.payload.table;
|
||||||
|
const fields = { channel, project, operation, table };
|
||||||
|
|
||||||
|
for (let rel of rels) {
|
||||||
|
if (rel.channels.includes(channel)) {
|
||||||
|
for (let url of rel.urls) {
|
||||||
|
for (let [key, data] of Object.entries(cache)) {
|
||||||
|
const match = key.match(url)?.slice(1);
|
||||||
|
if (match) {
|
||||||
|
if (rel.matches) {
|
||||||
|
if (rel.matches.every( (field, idx) => match[idx] == fields[field] )) {
|
||||||
|
console.log("DELETE ENTRY (MATCHES)", key);
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Delete unconditionally
|
||||||
|
console.log("DELETE ENTRY (UNCONDITIONAL)", key);
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rel.callback) {
|
||||||
|
for (let key of Object.keys(cache)) {
|
||||||
|
if (rel.callback(key, data)) {
|
||||||
|
console.log("DELETE ENTRY (CALLBACK)", key);
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watch (app) {
|
||||||
|
if (!app.locals?.etags) {
|
||||||
|
app.locals.etags = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function etagWatch (data) {
|
||||||
|
invalidateCache(data, app.locals.etags);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const client = await listen(channels, etagWatch);
|
||||||
|
console.log("ETag watch installed", client);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
console.log("ETag watch not installed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = watch;
|
||||||
@@ -15,5 +15,6 @@ module.exports = {
|
|||||||
info: require('./info'),
|
info: require('./info'),
|
||||||
meta: require('./meta'),
|
meta: require('./meta'),
|
||||||
openapi: require('./openapi'),
|
openapi: require('./openapi'),
|
||||||
rss: require('./rss')
|
rss: require('./rss'),
|
||||||
|
etag: require('./etag')
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user