Send SVG to clients, add simple caching

This commit is contained in:
Mattéo Delabre 2020-11-28 20:48:19 +01:00
parent bc93a2788e
commit 5456dcd898
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
5 changed files with 100 additions and 17 deletions

View File

@ -58,13 +58,13 @@ export const exploreVideos = async videoId =>
}; };
/** /**
* Convert a video graph to the DOT format. * Convert a video graph to the GraphViz format.
* *
* @param Object videosNodes Videos of the graph. * @param Object videosNodes Videos of the graph.
* @param Object nextVideos For each video, list of next videos. * @param Object nextVideos For each video, list of next videos.
* @return string DOT representation of the graph. * @return string GraphViz representation of the graph.
*/ */
export const graphToDOT = (videosNodes, nextVideos) => export const toGraphviz = (videosNodes, nextVideos) =>
{ {
// Convert videosNodes // Convert videosNodes
const nodesStr = Object.entries(videosNodes).map(([id, {title}]) => const nodesStr = Object.entries(videosNodes).map(([id, {title}]) =>
@ -80,7 +80,7 @@ export const graphToDOT = (videosNodes, nextVideos) =>
.join('\n') .join('\n')
).join('\n'); ).join('\n');
return `digraph maze { return `digraph "" {
${nodesStr} ${nodesStr}
${nextStr}}`; ${nextStr}}`;

35
lib/graphviz.mjs Normal file
View File

@ -0,0 +1,35 @@
import { spawn } from 'child_process';
import xml2js from 'xml2js';
const parser = new xml2js.Parser();
const builder = new xml2js.Builder({ headless: true });
const invokeDot = (input, format) => new Promise((resolve, reject) =>
{
const dot = spawn('dot', [`-T${format}`]);
let stdout = '';
dot.stdout.on('data', data => stdout += data);
dot.on('close', code => {
if (code !== 0)
{
reject(new Error(`dot returned error code ${code}`));
}
else
{
resolve(stdout);
}
});
dot.on('error', reject);
dot.stdin.write(input);
dot.stdin.end();
});
export const toSVG = async input =>
{
const svg = await invokeDot(input, 'svg');
const parsed = await parser.parseStringPromise(svg);
return builder.buildObject(parsed);
};

23
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "server", "name": "youtube-maze",
"version": "0.0.0", "version": "0.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -335,6 +335,11 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"send": { "send": {
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
@ -399,6 +404,20 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
}
},
"xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
} }
} }
} }

View File

@ -10,6 +10,7 @@
"debug": "~2.6.9", "debug": "~2.6.9",
"express": "~4.16.1", "express": "~4.16.1",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1",
"xml2js": "^0.4.23"
} }
} }

View File

@ -1,22 +1,50 @@
import express from 'express'; import express from 'express';
import { exploreVideos, graphToDOT } from '../lib/explore.mjs'; import { exploreVideos, toGraphviz } from '../lib/explore.mjs';
import { retryable } from '../lib/retry.mjs'; import { toSVG } from '../lib/graphviz.mjs';
const router = express.Router(); const router = express.Router();
export default router; export default router;
const retryExploreVideos = retryable(exploreVideos); const statusPending = Symbol('PENDING');
const statusError = Symbol('ERROR');
const cache = Object.create(null);
router.get('/:videoId', async (req, res) => { router.get('/:videoId', async (req, res) => {
const {videoId} = req.params;
if (videoId in cache)
{
if (cache[videoId] === statusPending)
{
res.header('Refresh', '5');
res.send('Exploration in progress… Please wait.');
}
else if (cache[videoId] === statusError)
{
res.status(500).send('Error');
}
else
{
res.send(cache[videoId]);
}
}
else
{
cache[videoId] = statusPending;
res.header('Refresh', '1');
res.send('Exploration in progress… Please wait.');
try try
{ {
const graph = await retryExploreVideos(req.params.videoId); const graph = await exploreVideos(req.params.videoId);
const dot = graphToDOT(...graph); const graphviz = toGraphviz(...graph);
res.send(dot); const svg = await toSVG(graphviz);
cache[videoId] = svg;
} }
catch (err) catch (err)
{ {
console.error(err); console.error(err);
res.status(500).send('Error'); cache[videoId] = statusError;
}
} }
}); });