Promiseの排他制御をしてくれるライブラリにasync-lockあります。
で、このライブラリには、ロックしているPromise
の実行が一定時間以上立った場合に Error
を投げてくれるTimeout
というオプションがあるのですが、こいつはTimeOutになってもエラーを投げるだけでロックを解除してはくれません。
なので、一定時間経過後に問答無用でロックを解除したい場合は、自分でロックを解除するコードを書かないといけないです。
まあ、処理が長過ぎるときのエラーハンドリングは、内部で実行されているPromise
関数が頑張れと言うことなのかもしれませんが、、それってTimeout
の意味なくない?ってなったので自作しました。(といっても本家のライブラリをラップしただけですが・・・)
TypeScriptで書いちゃってますが、型定義部分削ればJavaScriptでも使えると思います。
コード
実行にはasync-lock
が必要なので事前にインストールしてください
$ npm install async-lock
$ npm install --save-dev @types/async-lock
作成したラッパークラスは以下です
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
とか指定しなくても良くなってます。
const promiseLock = new PromiseLock()
const asyncHandler = () => {
// promiseLock.acquireの第一引数に指定した関数の実行が終わるまでロックする
promiseLock.acquire(async () => {
console.log('start')
await Promise処理()
console.log('finish')
})
}
第2引数にresolveTimeout
を指定することで、指定した時間経過後に強制的にロックを解除することができます。
本家と違ってError
もスローしません。(console.error()
は吐きますが・・・)
promiseLock.acquire(async () => {
console.log('start')
await Promise処理()
console.log('finish')
}, { resolveTimeout: 2000 }) // 2秒以上処理が終わらない場合は強制的にロックを解除する
一応キーも指定することができます。
promiseLock.acquire(async () => {
console.log('start')
await Promise処理()
console.log('finish')
},{ key: 'original_key' })
isBusy()
も使えます。これは、acquire
の中の関数が実行中であればtrue
を返す関数です。特にresolveTimeout
と組み合わせると、「処理が終わるまでは次の呼び出しを実行しないようにし、処理に時間がかかりすぎる場合はロックを解除する」みたいな動きができるので、ゲームのスクロール処理とか、「時間経過で処理を行いたいけど、遅延して実行するぐらいなら処理落ちしたほうが良い」みたいな時に結構使えます。
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なので気をつけてください)
const promiseLock = new PromiseLock({ maxPenging: 100 })
以上