From 296ef7bfc331d8766bc0144a83505efc67f41cbc Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Fri, 24 Apr 2020 18:26:04 +0200 Subject: [PATCH] never operate more than once on FS until cache expires --- cjs/index.js | 188 ++++++++++++++++++++++++----------------------- cjs/json.js | 29 ++++++++ cjs/stat.js | 30 ++++++++ esm/index.js | 191 +++++++++++++++++++++++++----------------------- esm/json.js | 28 +++++++ esm/stat.js | 29 ++++++++ package.json | 2 +- test/index.js | 41 +++++++++++ test/index.test | Bin 37453 -> 0 bytes test/server.cjs | 1 + 10 files changed, 354 insertions(+), 185 deletions(-) create mode 100644 cjs/json.js create mode 100644 cjs/stat.js create mode 100644 esm/json.js create mode 100644 esm/stat.js delete mode 100644 test/index.test diff --git a/cjs/index.js b/cjs/index.js index fdfe49c..5c5513d 100644 --- a/cjs/index.js +++ b/cjs/index.js @@ -1,13 +1,15 @@ 'use strict'; const { - createReadStream, stat, mkdir, readFile, unlink, existsSync, writeFileSync, watchFile, unwatchFile + createReadStream, mkdir, unlink, existsSync, writeFileSync, watch } = require('fs'); const {tmpdir} = require('os'); const {dirname, extname, join, resolve} = require('path'); const ucompress = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('ucompress')); -const {parse} = JSON; +const json = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('./json.js')); +const stat = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('./stat.js')); + const {compressed} = ucompress; const getPath = source => (source[0] === '/' ? source : resolve(source)); @@ -18,15 +20,12 @@ const internalServerError = res => { res.end(); }; -const readAndServe = (res, asset, IfNoneMatch) => { - readFile(asset + '.json', (err, data) => { +const readAndServe = (res, asset, cacheTimeout, IfNoneMatch) => + json(asset, cacheTimeout).then( + headers => serveFile(res, asset, headers, IfNoneMatch), /* istanbul ignore next */ - if (err) - internalServerError(res); - else - serveFile(res, asset, parse(data), IfNoneMatch); - }); -}; + () => internalServerError(res) + ); const serveFile = (res, asset, headers, IfNoneMatch) => { if (headers.ETag === IfNoneMatch) { @@ -42,15 +41,95 @@ const streamFile = (res, asset, headers) => { createReadStream(asset).pipe(res); }; -module.exports = ({source, dest, headers}) => { +module.exports = ({source, dest, headers, cacheTimeout: CT}) => { const SOURCE = getPath(source); const DEST = dest ? getPath(dest) : join(tmpdir(), 'ucdn'); const options = {createFiles: true, headers}; return (req, res, next) => { const path = req.url.replace(/\?.*$/, ''); const original = SOURCE + path; - stat(original, (err, stats) => { - if (err || !stats.isFile()) { + stat(original, CT).then( + ({lastModified, size}) => { + if (path === '/favicon.ico') + streamFile(res, original, { + 'Content-Length': size, + 'Content-Type': 'image/vnd.microsoft.icon', + ...headers + }); + else { + let asset = DEST + path; + let compression = ''; + const { + ['accept-encoding']: AcceptEncoding, + ['if-none-match']: IfNoneMatch, + ['if-modified-since']: IfModifiedSince + } = req.headers; + if (compressed.has(extname(path).toLowerCase())) { + switch (true) { + /* istanbul ignore next */ + case /\bbr\b/.test(AcceptEncoding): + compression = '.br'; + break; + case /\bgzip\b/.test(AcceptEncoding): + compression = '.gzip'; + break; + /* istanbul ignore next */ + case /\bdeflate\b/.test(AcceptEncoding): + compression = '.deflate'; + break; + } + asset += compression; + } + const create = () => { + const {length} = compression; + const compress = length ? asset.slice(0, -length) : asset; + const waitForIt = compress + '.wait'; + mkdir(dirname(waitForIt), {recursive: true}, err => { + /* istanbul ignore if */ + if (err) + internalServerError(res); + else if (existsSync(waitForIt)) + watch(waitForIt, andClose).on( + 'close', + () => readAndServe(res, asset, CT, IfNoneMatch) + ); + else { + try { + writeFileSync(waitForIt, path); + ucompress(original, compress, options) + .then( + () => { + unlink(waitForIt, err => { + /* istanbul ignore if */ + if (err) + internalServerError(res); + else + readAndServe(res, asset, CT, IfNoneMatch); + }); + }, + /* istanbul ignore next */ + () => unlink(waitForIt, () => internalServerError(res)) + ); + } + catch (o_O) { + /* istanbul ignore next */ + internalServerError(res); + } + } + }); + }; + json(asset, CT).then( + headers => { + if (lastModified === IfModifiedSince) + serveFile(res, asset, headers, IfNoneMatch); + else + create(); + }, + create + ); + } + }, + () => { if (next) next(); else { @@ -58,83 +137,10 @@ module.exports = ({source, dest, headers}) => { res.end(); } } - else if (path === '/favicon.ico') - streamFile(res, original, { - 'Content-Length': stats.size, - 'Content-Type': 'image/vnd.microsoft.icon', - ...headers - }); - else { - let asset = DEST + path; - let compression = ''; - const { - ['accept-encoding']: AcceptEncoding, - ['if-none-match']: IfNoneMatch, - ['if-modified-since']: IfModifiedSince - } = req.headers; - if (compressed.has(extname(path).toLowerCase())) { - switch (true) { - /* istanbul ignore next */ - case /\bbr\b/.test(AcceptEncoding): - compression = '.br'; - break; - case /\bgzip\b/.test(AcceptEncoding): - compression = '.gzip'; - break; - /* istanbul ignore next */ - case /\bdeflate\b/.test(AcceptEncoding): - compression = '.deflate'; - break; - } - asset += compression; - } - readFile(asset + '.json', (err, data) => { - // if there was no error, be sure the source file is still the same - if (!err) { - if (new Date(stats.mtimeMs).toUTCString() === IfModifiedSince) { - serveFile(res, asset, parse(data), IfNoneMatch); - return; - } - } - // if the file was modified, re-optimize it, assuming it changed too - const {length} = compression; - const compress = length ? asset.slice(0, -length) : asset; - const waitForIt = compress + '.wait'; - mkdir(dirname(waitForIt), {recursive: true}, err => { - /* istanbul ignore next */ - if (err) - internalServerError(res); - else if (existsSync(waitForIt)) - watchFile(waitForIt, () => { - unwatchFile(waitForIt); - readAndServe(res, asset, IfNoneMatch); - }); - else { - try { - writeFileSync(waitForIt, path); - ucompress(original, compress, options) - .then( - () => { - unlink(waitForIt, err => { - /* istanbul ignore next */ - if (err) - internalServerError(res); - else - readAndServe(res, asset, IfNoneMatch); - }); - }, - /* istanbul ignore next */ - () => unlink(waitForIt, () => internalServerError(res)) - ); - } - catch (o_O) { - /* istanbul ignore next */ - internalServerError(res); - } - } - }); - }); - } - }); + ); }; }; + +function andClose() { + this.close(); +} diff --git a/cjs/json.js b/cjs/json.js new file mode 100644 index 0000000..e5b1efe --- /dev/null +++ b/cjs/json.js @@ -0,0 +1,29 @@ +'use strict'; +const {readFile} = require('fs'); + +const umap = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('umap')); + +const {parse} = JSON; + +const _ = new Map; +const $ = umap(_); + +const clear = asset => { + _.delete(asset); +}; + +module.exports = (asset, timeout = 1000) => ( + $.get(asset) || + $.set(asset, new Promise((res, rej) => { + readFile(asset + '.json', (err, data) => { + if (err) { + _.delete(asset); + rej(); + } + else { + res(parse(data)); + setTimeout(clear, timeout, asset); + } + }); + })) +); diff --git a/cjs/stat.js b/cjs/stat.js new file mode 100644 index 0000000..1175034 --- /dev/null +++ b/cjs/stat.js @@ -0,0 +1,30 @@ +'use strict'; +const {stat} = require('fs'); + +const umap = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('umap')); + +const _ = new Map; +const $ = umap(_); + +const clear = asset => { + _.delete(asset); +}; + +module.exports = (asset, timeout = 1000) => ( + $.get(asset) || + $.set(asset, new Promise((res, rej) => { + stat(asset, (err, stats) => { + if (err || !stats.isFile()) { + _.delete(asset); + rej(); + } + else { + setTimeout(clear, timeout, asset); + res({ + lastModified: new Date(stats.mtimeMs).toUTCString(), + size: stats.size + }); + } + }); + })) +); diff --git a/esm/index.js b/esm/index.js index 30e9a2a..7a52fea 100644 --- a/esm/index.js +++ b/esm/index.js @@ -1,14 +1,15 @@ import { - createReadStream, stat, mkdir, readFile, unlink, - existsSync, writeFileSync, - watchFile, unwatchFile + createReadStream, mkdir, unlink, + existsSync, writeFileSync, watch } from 'fs'; import {tmpdir} from 'os'; import {dirname, extname, join, resolve} from 'path'; import ucompress from 'ucompress'; -const {parse} = JSON; +import json from './json.js'; +import stat from './stat.js'; + const {compressed} = ucompress; const getPath = source => (source[0] === '/' ? source : resolve(source)); @@ -19,15 +20,12 @@ const internalServerError = res => { res.end(); }; -const readAndServe = (res, asset, IfNoneMatch) => { - readFile(asset + '.json', (err, data) => { +const readAndServe = (res, asset, cacheTimeout, IfNoneMatch) => + json(asset, cacheTimeout).then( + headers => serveFile(res, asset, headers, IfNoneMatch), /* istanbul ignore next */ - if (err) - internalServerError(res); - else - serveFile(res, asset, parse(data), IfNoneMatch); - }); -}; + () => internalServerError(res) + ); const serveFile = (res, asset, headers, IfNoneMatch) => { if (headers.ETag === IfNoneMatch) { @@ -43,15 +41,95 @@ const streamFile = (res, asset, headers) => { createReadStream(asset).pipe(res); }; -export default ({source, dest, headers}) => { +export default ({source, dest, headers, cacheTimeout: CT}) => { const SOURCE = getPath(source); const DEST = dest ? getPath(dest) : join(tmpdir(), 'ucdn'); const options = {createFiles: true, headers}; return (req, res, next) => { const path = req.url.replace(/\?.*$/, ''); const original = SOURCE + path; - stat(original, (err, stats) => { - if (err || !stats.isFile()) { + stat(original, CT).then( + ({lastModified, size}) => { + if (path === '/favicon.ico') + streamFile(res, original, { + 'Content-Length': size, + 'Content-Type': 'image/vnd.microsoft.icon', + ...headers + }); + else { + let asset = DEST + path; + let compression = ''; + const { + ['accept-encoding']: AcceptEncoding, + ['if-none-match']: IfNoneMatch, + ['if-modified-since']: IfModifiedSince + } = req.headers; + if (compressed.has(extname(path).toLowerCase())) { + switch (true) { + /* istanbul ignore next */ + case /\bbr\b/.test(AcceptEncoding): + compression = '.br'; + break; + case /\bgzip\b/.test(AcceptEncoding): + compression = '.gzip'; + break; + /* istanbul ignore next */ + case /\bdeflate\b/.test(AcceptEncoding): + compression = '.deflate'; + break; + } + asset += compression; + } + const create = () => { + const {length} = compression; + const compress = length ? asset.slice(0, -length) : asset; + const waitForIt = compress + '.wait'; + mkdir(dirname(waitForIt), {recursive: true}, err => { + /* istanbul ignore if */ + if (err) + internalServerError(res); + else if (existsSync(waitForIt)) + watch(waitForIt, andClose).on( + 'close', + () => readAndServe(res, asset, CT, IfNoneMatch) + ); + else { + try { + writeFileSync(waitForIt, path); + ucompress(original, compress, options) + .then( + () => { + unlink(waitForIt, err => { + /* istanbul ignore if */ + if (err) + internalServerError(res); + else + readAndServe(res, asset, CT, IfNoneMatch); + }); + }, + /* istanbul ignore next */ + () => unlink(waitForIt, () => internalServerError(res)) + ); + } + catch (o_O) { + /* istanbul ignore next */ + internalServerError(res); + } + } + }); + }; + json(asset, CT).then( + headers => { + if (lastModified === IfModifiedSince) + serveFile(res, asset, headers, IfNoneMatch); + else + create(); + }, + create + ); + } + }, + () => { if (next) next(); else { @@ -59,83 +137,10 @@ export default ({source, dest, headers}) => { res.end(); } } - else if (path === '/favicon.ico') - streamFile(res, original, { - 'Content-Length': stats.size, - 'Content-Type': 'image/vnd.microsoft.icon', - ...headers - }); - else { - let asset = DEST + path; - let compression = ''; - const { - ['accept-encoding']: AcceptEncoding, - ['if-none-match']: IfNoneMatch, - ['if-modified-since']: IfModifiedSince - } = req.headers; - if (compressed.has(extname(path).toLowerCase())) { - switch (true) { - /* istanbul ignore next */ - case /\bbr\b/.test(AcceptEncoding): - compression = '.br'; - break; - case /\bgzip\b/.test(AcceptEncoding): - compression = '.gzip'; - break; - /* istanbul ignore next */ - case /\bdeflate\b/.test(AcceptEncoding): - compression = '.deflate'; - break; - } - asset += compression; - } - readFile(asset + '.json', (err, data) => { - // if there was no error, be sure the source file is still the same - if (!err) { - if (new Date(stats.mtimeMs).toUTCString() === IfModifiedSince) { - serveFile(res, asset, parse(data), IfNoneMatch); - return; - } - } - // if the file was modified, re-optimize it, assuming it changed too - const {length} = compression; - const compress = length ? asset.slice(0, -length) : asset; - const waitForIt = compress + '.wait'; - mkdir(dirname(waitForIt), {recursive: true}, err => { - /* istanbul ignore next */ - if (err) - internalServerError(res); - else if (existsSync(waitForIt)) - watchFile(waitForIt, () => { - unwatchFile(waitForIt); - readAndServe(res, asset, IfNoneMatch); - }); - else { - try { - writeFileSync(waitForIt, path); - ucompress(original, compress, options) - .then( - () => { - unlink(waitForIt, err => { - /* istanbul ignore next */ - if (err) - internalServerError(res); - else - readAndServe(res, asset, IfNoneMatch); - }); - }, - /* istanbul ignore next */ - () => unlink(waitForIt, () => internalServerError(res)) - ); - } - catch (o_O) { - /* istanbul ignore next */ - internalServerError(res); - } - } - }); - }); - } - }); + ); }; }; + +function andClose() { + this.close(); +} diff --git a/esm/json.js b/esm/json.js new file mode 100644 index 0000000..aded27b --- /dev/null +++ b/esm/json.js @@ -0,0 +1,28 @@ +import {readFile} from 'fs'; + +import umap from 'umap'; + +const {parse} = JSON; + +const _ = new Map; +const $ = umap(_); + +const clear = asset => { + _.delete(asset); +}; + +export default (asset, timeout = 1000) => ( + $.get(asset) || + $.set(asset, new Promise((res, rej) => { + readFile(asset + '.json', (err, data) => { + if (err) { + _.delete(asset); + rej(); + } + else { + res(parse(data)); + setTimeout(clear, timeout, asset); + } + }); + })) +); diff --git a/esm/stat.js b/esm/stat.js new file mode 100644 index 0000000..4b2c56d --- /dev/null +++ b/esm/stat.js @@ -0,0 +1,29 @@ +import {stat} from 'fs'; + +import umap from 'umap'; + +const _ = new Map; +const $ = umap(_); + +const clear = asset => { + _.delete(asset); +}; + +export default (asset, timeout = 1000) => ( + $.get(asset) || + $.set(asset, new Promise((res, rej) => { + stat(asset, (err, stats) => { + if (err || !stats.isFile()) { + _.delete(asset); + rej(); + } + else { + setTimeout(clear, timeout, asset); + res({ + lastModified: new Date(stats.mtimeMs).toUTCString(), + size: stats.size + }); + } + }); + })) +); diff --git a/package.json b/package.json index 3c92e2d..82c24ef 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "npm run cjs && npm run test", "cjs": "ascjs --no-default esm cjs", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "test": "rm -rf test/dest && nyc node test/index.js" + "test": "rm -rf test/dest && nyc node test/index.js && rm test/index.test" }, "keywords": [ "ucompress", diff --git a/test/index.js b/test/index.js index fb6c4d3..0efa8e1 100644 --- a/test/index.js +++ b/test/index.js @@ -148,6 +148,7 @@ Promise.resolve('\x1b[1mµcdn\x1b[0m') })) .then(name => { requestHandler = cdn({ + cacheTimeout: 100, source: './test/source' }); return name; @@ -215,5 +216,45 @@ Promise.resolve('\x1b[1mµcdn\x1b[0m') }) ); })) + + .then(name => new Promise(resolve => { + console.log(name); + const path = '/benja-dark.svg'; + const request = createRequest(path); + request.headers.acceptEncoding = ''; + let i = 0; + const done = () => { + if (++i === 2) + resolve(path); + }; + requestHandler( + request, + createResponse(operations => { + console.assert(operations.length === 2, 'correct amount of operations'); + const [code, headers] = operations.shift(); + const content = operations.shift(); + console.assert(content.length < 1, 'correct content'); + console.assert(code === 200, 'correct code'); + console.assert(headers['Content-Length'] === 2478, 'correct length'); + console.assert(headers['Content-Type'] === 'image/svg+xml', 'correct mime'); + console.assert(headers['ETag'] === '"9ae-WAa0uuWT+I9+hj77"', 'correct ETag'); + done(); + }) + ); + requestHandler( + request, + createResponse(operations => { + console.assert(operations.length === 2, 'correct amount of operations'); + const [code, headers] = operations.shift(); + const content = operations.shift(); + console.assert(content.length < 1, 'correct content'); + console.assert(code === 200, 'correct code'); + console.assert(headers['Content-Length'] === 2478, 'correct length'); + console.assert(headers['Content-Type'] === 'image/svg+xml', 'correct mime'); + console.assert(headers['ETag'] === '"9ae-WAa0uuWT+I9+hj77"', 'correct ETag'); + done(); + }) + ); + })) .then(console.log) ; diff --git a/test/index.test b/test/index.test deleted file mode 100644 index ea71b6141048862202a74f4136e8c460522f65e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37453 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 z 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 diff --git a/test/server.cjs b/test/server.cjs index abd331e..60aa6f6 100644 --- a/test/server.cjs +++ b/test/server.cjs @@ -3,6 +3,7 @@ const {join} = require('path'); const cdn = require('../cjs'); const callback = cdn({ + cacheTimeout: 10000, source: join(__dirname, 'source'), dest: join(__dirname, 'dest') });