特定のURLへアクセスしたさい、コマンドラインの実行や、ファイルのコンパイルを行い、成果物を返すようなAPIを作成したい場合があります。
たとえば、以下の例では、/articles/
にアクセスすると、index.md
をindex.html
にコンパイルした結果を返します。
pct/
git clone https://github.com/59798/promise-cache-task.git pct
cd pct
git checkout e0922b6b4e3caf8f8610ef3e074eb37ce3314072
npm install
pct/src/index.js
// Dependencies
import Bluebird from 'bluebird';
import { Router as expressRouter } from 'express';
import fsOrigin from 'fs';
import marked from 'marked';
// Module enhancement
const fs = Bluebird.promisifyAll(fsOrigin);
const markedAsync = Bluebird.promisify(marked);
// Public
export default (cwd) => {
const router = expressRouter();
router.use((req, res, next) => {
let filePath = req.url.slice(1);
if (filePath === '' || filePath.match(/\/$/)) {
filePath += 'index';
}
const fileName = `${cwd}/${filePath}.md`;
const cacheName = `${cwd}/${filePath}.html`;
const notFound = fs.existsSync(fileName) === false;
const useCache = fs.existsSync(cacheName);
if (notFound) {
return next();
}
if (useCache) {
return res.sendFile(cacheName);
}
console.log('以降の処理は重いので1度だけ実行したい');
return fs.readFileAsync(fileName)
.then((data) => markedAsync(data.toString()))
.then((cache) => {
const trimedCache = cache.trim();// 末尾"\n"の削除
return fs.writeFileAsync(cacheName, trimedCache)
.then(() => {
res.set('content-type', 'text/html');
res.end(trimedCache);
});
});
});
return router;
};
test/index.js
// Dependencies
import middleware from '../src';
import express from 'express';
import caravan from 'caravan';
import assert from 'power-assert';
import del from 'del';
// Environment
const cwd = `${__dirname}/fixtures/public`;
const port = process.env.PORT || 59798;
// Specs
describe('markedown compile server', () => {
let server;
before((done) => {
const app = express();
app.use('/articles', middleware(cwd));
server = app.listen(port, done);
});
after((done) => (
server.close(() => {
del(`${cwd}/**/*.html`).then(() => done());
})
));
it('markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない', () => {
const concurrency = 100;
const urls = [];
for (let i = 0; i < concurrency; i++) {
urls.push(`http://localhost:${port}/articles`);
}
return caravan(urls, { concurrency })
.progress((progress) => {
assert.equal(progress.value, '<p><strong>要反省である</strong></p>');
})
.then((responses) => {
assert.equal(responses.length, concurrency);
});
});
});
上記コードは、コンパイル後のindex.html
を利用することを想定していますが、テストでは100リクエストをほぼ同時に行うため、重いタスクを何度も実行してしまっていることが確認できます。
pct/
npm test
# markedown compile server
# 以降の処理は重いので1度だけ実行したい
# 以降の処理は重いので1度だけ実行したい
# 以降の処理は...
# ...
# ✓ markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない
# 1 passing (159ms)
そこで、タスクの進捗をPromiseに保存し、2度目以降の要求を保留にすることで、これを回避することができます。
pct/
git checkout master
npm test
# markedown compile server
# 以降の処理は重いので1度だけ実行したい
# ✓ markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない
# 1 passing (59ms)