Send SVG to clients, add simple caching
This commit is contained in:
parent
bc93a2788e
commit
5456dcd898
|
@ -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 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
|
||||
const nodesStr = Object.entries(videosNodes).map(([id, {title}]) =>
|
||||
|
@ -80,7 +80,7 @@ export const graphToDOT = (videosNodes, nextVideos) =>
|
|||
.join('\n')
|
||||
).join('\n');
|
||||
|
||||
return `digraph maze {
|
||||
return `digraph "" {
|
||||
${nodesStr}
|
||||
|
||||
${nextStr}}`;
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "server",
|
||||
"version": "0.0.0",
|
||||
"name": "youtube-maze",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -335,6 +335,11 @@
|
|||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"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": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
|
||||
|
@ -399,6 +404,20 @@
|
|||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"morgan": "~1.9.1",
|
||||
"node-fetch": "^2.6.1"
|
||||
"node-fetch": "^2.6.1",
|
||||
"xml2js": "^0.4.23"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,50 @@
|
|||
import express from 'express';
|
||||
import { exploreVideos, graphToDOT } from '../lib/explore.mjs';
|
||||
import { retryable } from '../lib/retry.mjs';
|
||||
import { exploreVideos, toGraphviz } from '../lib/explore.mjs';
|
||||
import { toSVG } from '../lib/graphviz.mjs';
|
||||
|
||||
const router = express.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) => {
|
||||
try
|
||||
const {videoId} = req.params;
|
||||
|
||||
if (videoId in cache)
|
||||
{
|
||||
const graph = await retryExploreVideos(req.params.videoId);
|
||||
const dot = graphToDOT(...graph);
|
||||
res.send(dot);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
else
|
||||
{
|
||||
console.error(err);
|
||||
res.status(500).send('Error');
|
||||
cache[videoId] = statusPending;
|
||||
res.header('Refresh', '1');
|
||||
res.send('Exploration in progress… Please wait.');
|
||||
|
||||
try
|
||||
{
|
||||
const graph = await exploreVideos(req.params.videoId);
|
||||
const graphviz = toGraphviz(...graph);
|
||||
const svg = await toSVG(graphviz);
|
||||
cache[videoId] = svg;
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.error(err);
|
||||
cache[videoId] = statusError;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue