fsWatchFile.mjs

import path from 'path'
import events from 'events'
import chokidar from 'chokidar'
import get from 'lodash-es/get.js'
import ispint from './ispint.mjs'
import isbol from './isbol.mjs'
import cint from './cint.mjs'
import fsIsFile from './fsIsFile.mjs'


/**
 * 後端nodejs基於chokidar提供偵測檔案內容變更或出現或消失事件
 *
 * 因使用chokidar,變更至少要3秒才能監測,不適用於頻繁觸發事件之工作
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/fsWatchFile.test.mjs Github}
 * @memberOf wsemi
 * @param {String} fp 輸入偵測檔案路徑字串
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.polling=false] 輸入是否使用輪循布林值,代表chokidar的usePolling,預設為false
 * @param {Integer} [opt.timeInterval=100] 輸入當polling為true時偵測檔案變更間隔時間整數,代表chokidar開啟polling時的interval,單位為毫秒ms,預設為100
 * @param {Integer} [opt.timeBinaryInterval=300] 輸入當polling為true時偵測二進位檔案變更間隔時間整數,代表chokidar開啟polling時的binaryInterval,單位為毫秒ms,預設為300
 * @returns {Object} 回傳事件物件,包含on、clear函數,on可進行監聽change事件,clear為停止全部監聽,不須輸入
 * @example
 * need test in nodejs.
 *
 * import getFileName from './src/getFileName.mjs'
 * import fsDeleteFile from './src/fsDeleteFile.mjs'
 * import fsRenameFile from './src/fsRenameFile.mjs'
 * import fsWatchFile from './src/fsWatchFile.mjs'
 *
 * let test = async () => {
 *     return new Promise((resolve, reject) => {
 *         let ms = []
 *
 *         let fp = './_test_fsWatchFile.txt'
 *
 *         fsDeleteFile(fp)
 *
 *         let ev = fsWatchFile(fp)
 *         ev.on('change', (msg) => {
 *             console.log(msg.type, getFileName(msg.fp))
 *             ms.push({ type: msg.type, fp: getFileName(msg.fp) })
 *         })
 *
 *         setTimeout(() => {
 *             fs.writeFileSync(fp, 'abc', 'utf8')
 *         }, 1)
 *
 *         setTimeout(() => {
 *             fsRenameFile(fp, fp + '.tmp')
 *         }, 3000)
 *
 *         setTimeout(() => {
 *             fsRenameFile(fp + '.tmp', fp)
 *         }, 6000)
 *
 *         setTimeout(() => {
 *             fs.writeFileSync(fp, 'def', 'utf8')
 *         }, 9000)
 *
 *         setTimeout(() => {
 *             fsDeleteFile(fp)
 *         }, 12000)
 *
 *         setTimeout(() => {
 *             ev.clear()
 *             console.log('ms', ms)
 *             resolve(ms)
 *         }, 15000)
 *
 *     })
 * }
 * test()
 *     .catch(() => {})
 * // add _test_fsWatchFile.txt
 * // unlink _test_fsWatchFile.txt
 * // add _test_fsWatchFile.txt
 * // change _test_fsWatchFile.txt
 * // unlink _test_fsWatchFile.txt
 * // ms [
 * //   { type: 'add', fp: '_test_fsWatchFile.txt' },
 * //   { type: 'unlink', fp: '_test_fsWatchFile.txt' },
 * //   { type: 'add', fp: '_test_fsWatchFile.txt' },
 * //   { type: 'change', fp: '_test_fsWatchFile.txt' },
 * //   { type: 'unlink', fp: '_test_fsWatchFile.txt' }
 * // ]
 *
 */
function fsWatchFile(fp, opt = {}) {

    //polling
    let polling = get(opt, 'polling')
    if (!isbol(polling)) {
        polling = false
    }

    //timeInterval
    let timeInterval = get(opt, 'timeInterval')
    if (!ispint(timeInterval)) {
        timeInterval = 100
    }
    timeInterval = cint(timeInterval)

    //timeBinaryInterval
    let timeBinaryInterval = get(opt, 'timeBinaryInterval')
    if (!ispint(timeBinaryInterval)) {
        timeBinaryInterval = 300
    }
    timeBinaryInterval = cint(timeBinaryInterval)

    //ev
    let ev = new events.EventEmitter()

    //fpSpe
    let fpSpe = fp

    //timer
    let watcher = null
    let t = setInterval(() => {

        //check
        if (watcher !== null) {
            return
        }

        //check
        if (!fsIsFile(fpSpe)) {
            return
        }

        //watcher
        watcher = chokidar.watch(fpSpe, {
            // persistent: true,
            // ignoreInitial: false,
            usePolling: polling,
            interval: timeInterval,
            binaryInterval: timeBinaryInterval,
            awaitWriteFinish: true, //須比較多延遲偵測檔案是否變更完成, 但對於連鎖驅動比較保險
            // depth: undefined,
        })

        //on
        watcher
            .on('all', (type, fp, stats) => {
                // console.log(type, fp, stats)
                //type=add,change,unlink
                //注意當刪除監聽的fp後再新增同名檔案, 或更名監聽的fp再更名回同名檔案, 會觸發type='change'而不是'add'

                //fp
                fp = path.resolve(fp)

                //emit
                ev.emit('change', { type, fp, stats })

            })


    }, timeInterval)

    //unWatch
    let unWatch = () => {
        // console.log('call unWatch')
        if (watcher === null) {
            return
        }
        try {
            watcher.unwatch(fpSpe)
        }
        catch (err) {
            console.log(err)
        }
        watcher.close()
            // .then(() => {})
            .catch((err) => {
                console.log(err)
            })
            .finally(() => {
                watcher = null
                // console.log('watcher=null')
            })
    }

    //clear
    let clear = () => {
        unWatch()
        clearInterval(t)
    }

    //save
    ev.clear = clear

    return ev
}

export default fsWatchFile