同時アクセス時に重いタスクを１度だけ実行する

特定のURLへアクセスしたさい、コマンドラインの実行や、ファイルのコンパイルを行い、成果物を返すようなAPIを作成したい場合があります。

たとえば、以下の例では、`/articles/`にアクセスすると、`index.md`を`index.html`にコンパイルした結果を返します。

```bash:pct/
git clone https://github.com/59798/promise-cache-task.git pct
cd pct
git checkout e0922b6b4e3caf8f8610ef3e074eb37ce3314072

npm install
```

```js: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('以降の処理は重いので１度だけ実行したい');

    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;
};
```

```js: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`を利用することを想定していますが、テストでは１００リクエストをほぼ同時に行うため、重いタスクを何度も実行してしまっていることが確認できます。

```bash:pct/
npm test
# markedown compile server
# 以降の処理は重いので１度だけ実行したい
# 以降の処理は重いので１度だけ実行したい
# 以降の処理は...
# ...
#   ✓ markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない
# 1 passing (159ms)
```

そこで、[タスクの進捗をPromiseに保存し、２度目以降の要求を保留にする](https://github.com/59798/promise-cache-task/commit/37748f618ce3f37ad19e5ba35f5beca1757f30ca#diff-1)ことで、これを回避することができます。

```bash:pct/
git checkout master
npm test
# markedown compile server
# 以降の処理は重いので１度だけ実行したい
#   ✓ markdownをhtmlとしてコンパイル返す。htmlはキャッシュとして保存し、以降はコンパイルしない
# 1 passing (59ms)
```