タイムアウトした時にちゃんとロックを解除してくれる async-lockを作る

Promiseの排他制御をしてくれるライブラリに[async-lock](https://www.npmjs.com/package/async-lock)あります。

で、このライブラリには、ロックしている`Promise`の実行が一定時間以上立った場合に `Error` を投げてくれる`Timeout`というオプションがあるのですが、こいつはTimeOutになってもエラーを投げるだけでロックを解除してはくれません。

なので、一定時間経過後に問答無用でロックを解除したい場合は、自分でロックを解除するコードを書かないといけないです。

まあ、処理が長過ぎるときのエラーハンドリングは、内部で実行されている`Promise`関数が頑張れと言うことなのかもしれませんが、、それって`Timeout`の意味なくない？ってなったので自作しました。（といっても本家のライブラリをラップしただけですが・・・）

TypeScriptで書いちゃってますが、型定義部分削ればJavaScriptでも使えると思います。

### コード

実行には`async-lock`が必要なので事前にインストールしてください

```
$ npm install async-lock
$ npm install --save-dev  @types/async-lock
```

作成したラッパークラスは以下です

```.ts
import AsyncLock from 'async-lock'

export class PromiseLock {

    asyncLock: AsyncLock
    constructor(option?: AsyncLockOptions) {
        this.asyncLock = new AsyncLock(option)
    }

    acquire(func: Function, options: AquireOptions = {}) {

        let { resolveTimeout, key } = options
        if (!key) {
            key = '__DEFAULT_KEY__'
        }

        return this.asyncLock.acquire(key, () => {
            return new Promise(async resolve => {
                let isResolved = false
                if (resolveTimeout) {
                    setTimeout(() => {
                        if (!isResolved) {
                            console.error(`${resolveTimeout} ms passed. over resolveTimeout. unlock promiseLock`)
                            resolve()
                        }
                    }, resolveTimeout)
                }
                await func()
                resolve()
                isResolved = true
            })
        })
    }

    isBusy(key = '__DEFAULT_KEY__') {
        return this.asyncLock.isBusy(key)
    }
}

interface AsyncLockOptions {
    timeout?: number;
    maxPending?: number;
    domainReentrant?: boolean;
    Promise?: any;
}

interface AquireOptions {
    resolveTimeout?: number
    key?: string | string[]
}
```

### 使い方

使い方は以下のような感じです。だいたい本家と一緒ですが、不要なら`key`とか指定しなくても良くなってます。

```.ts
    const promiseLock = new PromiseLock()

    const asyncHandler = () => { 
        // promiseLock.acquireの第一引数に指定した関数の実行が終わるまでロックする
        promiseLock.acquire(async () => {
            console.log('start')
            await Promise処理()
            console.log('finish')
        })
    }
```

第2引数に`resolveTimeout`を指定することで、指定した時間経過後に強制的にロックを解除することができます。
本家と違って`Error`もスローしません。(`console.error()`は吐きますが・・・)

```.ts
        promiseLock.acquire(async () => {
            console.log('start')
            await Promise処理()
            console.log('finish')
        }, { resolveTimeout: 2000 }) // 2秒以上処理が終わらない場合は強制的にロックを解除する
```

一応キーも指定することができます。

```.ts
        promiseLock.acquire(async () => {
            console.log('start')
            await Promise処理()
            console.log('finish')
        },{ key: 'original_key' })
```

`isBusy()`も使えます。これは、`acquire`の中の関数が実行中であれば`true`を返す関数です。特に`resolveTimeout`と組み合わせると、「処理が終わるまでは次の呼び出しを実行しないようにし、処理に時間がかかりすぎる場合はロックを解除する」みたいな動きができるので、ゲームのスクロール処理とか、「時間経過で処理を行いたいけど、遅延して実行するぐらいなら処理落ちしたほうが良い」みたいな時に結構使えます。

```.ts
    const handler = () => {
        // acquireの処理中は次の処理を受け付けない
        if (promiseLock.isBusy()) {
            console.log('handler is busy. skip exec function')
            return
        }

        promiseLock.acquire(async () => {
            console.log('start')
            await Promise処理()
            console.log('finish')
        }, { resolveTimeout: 2000 }) // 2秒以上処理が遅延する場合には、強制的にロックを解除し、次の処理を実行する
    }
```

本家のoptionを使いたい場合は、newする時に指定することもできます。(ただここで指定できる`timeout`は本家のエラーを吐くだけのtimeoutなので気をつけてください)

```.ts
    const promiseLock = new PromiseLock({ maxPenging: 100 })
```

以上