From 5456dcd898288ca5207139bc9712accfefb2b718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=C3=A9o=20Delabre?= Date: Sat, 28 Nov 2020 20:48:19 +0100 Subject: [PATCH] Send SVG to clients, add simple caching --- lib/explore.mjs | 8 ++++---- lib/graphviz.mjs | 35 ++++++++++++++++++++++++++++++++++ package-lock.json | 23 +++++++++++++++++++++-- package.json | 3 ++- routes/mazes.mjs | 48 +++++++++++++++++++++++++++++++++++++---------- 5 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 lib/graphviz.mjs diff --git a/lib/explore.mjs b/lib/explore.mjs index a473d54..ddfa724 100644 --- a/lib/explore.mjs +++ b/lib/explore.mjs @@ -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}}`; diff --git a/lib/graphviz.mjs b/lib/graphviz.mjs new file mode 100644 index 0000000..212e036 --- /dev/null +++ b/lib/graphviz.mjs @@ -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); +}; diff --git a/package-lock.json b/package-lock.json index 88ccb56..1b5f7a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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==" } } } diff --git a/package.json b/package.json index 4c2a763..012e192 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/routes/mazes.mjs b/routes/mazes.mjs index ffb2413..48ef547 100644 --- a/routes/mazes.mjs +++ b/routes/mazes.mjs @@ -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; + } } });