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 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}}`;
|
||||||
|
|
|
@ -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",
|
"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=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue