APIのプロミス化

module.exports = function(callbackBasedApi) {
    return function promisified() {
        const args = [].slice.call(arguments)
        
        // プロミスオブジェクトを作成し、即座に返す
        return new Promise((resolve, reject) => {
            // promisified関数に渡された引数列の末尾に
            // 処理結果通知用のコールバック関数を追加
            args.push((err, result) => {
                if (err) {
                    return reject(err)
                }
                
                if (arguments.length <= 2) {
                    resolve(result)
                } else {
                    resolve([].slice.call(arguments, 1))
                }
            })
            
            // 作成した引数列でcallbackBasedApiを呼び出し
            callbackBasedApi.apply(null, args)
        })
    }
}

Iteratorと再帰による逐次処理

ウェブスパイダの改良

const path = require('path')
const utilities = require('./utilities')

const request = utilities.promisify(require('request'))
const mkdirp = utilities.promisify(require('mkdirp'))

const fs = require('fs')
const readFile = utilities.promisify(fs.readFile)
const writeFile = utilities.promisify(fs.writeFile)

// URLをダウンロードし、ファイルを保存
function download(url, filename) {
    console.log(`Downloading ${url}`)
    
    let body
    
    return request(url)
        .then(response => {
            body = response.body
            return mkdirp(path.dirname(filename))
        })
        .then(() => writeFile(filename, body))
        .then(() => {
            console.log(`Downloaded and saved: ${url}`)
            return body
        })
}

function spiderLinks(currentUrl, body, nesting) {
    // 空のプロミスを生成(メソッドチェーンを構築するための開始点)
    // このプロミスはundefinedとともに即座にresolveされる
    let promise = Promise.resolve()
    
    if (nesting === 0) {
        return promise
    }
    
    let links = utilities.getPageLinks(currentUrl, body)
    
    links.forEach(link => {
        promise = promise.then(() => spider(link, nesting - 1))
    })
    
    return promise
}

function spider(url, nesting) {
    let filename = utilities.urlToFilename(url)
    
    return readFile(filename, 'utf8')
        .then(
            (body) => (spiderLinks(url, body, nesting)),
            (err) => {
                if (err.code !== 'ENOENT') {
                    throw err
                }
                
                return download(url, filename)
                    .then(body => spiderLinks(url, body, nesting))
            }
        )
}

/* -------------------------------------------------------------------------- */

// コマンドラインから受け取ったURLを引数として渡してspiderを呼び出し
spider(process.argv[2], 1)
    .then(() => console.log('Download complete'))
    .catch(err => console.log(err))

// node index <https://tetracalibers.net>

パターンの勘所

let tasks = [ /** タスク */]
let promise = Promise.resolve()

tasks.forEach(task => {
    promise = promise.then(() => {
        return task()
    })
})

promise.then(() => {
    console.log('全てのタスクが完了しました')
})

ループによる並行処理

ウェブスパイダの改良