uci-utils-watcher/src/watcher.js

194 lines
6.3 KiB
JavaScript

// native imports
import { EventEmitter as Emitter } from 'events'
import path from 'path'
// third party imports
import { watch } from 'chokidar'
import debounce from 'debounce-fn'
// UCI imports
import readIgnoreLists from '@uci-utils/read-lines'
import logger from '@uci-utils/logger'
let log = {}
const READY_TIMEOUT = 10000 //default
class Watcher extends Emitter {
constructor(opts={}) {
super()
log = logger({ package:'@uci-utils/watcher', class:'Watcher', file:'src/watcher.js'})
opts.unlinkDir = 'unlinkDir' in opts ? opts.unlinkDir : true // delete file even on by default
this.opts = opts
this.timeout = process.env.READY_TIMEOUT || opts.readyTimeout || READY_TIMEOUT
this._ignored = opts.ignored ? (Array.isArray(opts.ignored) ? opts.ignored : opts.ignored.split(',')) : []
this.handler = opts.handler || _handler
this._ready=false
this._watching=false
return this
}
get watching () {
return this._watching
}
get ready () {
return this._ready
}
registerHandler(func,opts) {
opts = Object.assign({},this.opts,opts)
if (!this._watcher) return 'failed: watcher not initialized'
if (typeof func ==='function') this.handler = func
else if (func) opts = func
opts = Object.assign({},this.opts,opts)
let handler
if (opts.debounce)
handler = debounce((type, file) => {
log.debug(`waited : ${opts.debounce}ms before calling handler`)
log.info('debounced handler, only last event is emitted')
this.handler.call(this,type,file)
},{wait:opts.debounce})
else
handler= (type,file) => {this.handler.call(this,type,file)}
this._watcher.removeAllListeners()
if (opts.delDir) this._watcher.on('unlinkDir', handler.bind(this, 'dir-deleted'))
if (opts.addDir) this._watcher.on('addDir', handler.bind(this, 'dir-added'))
this._watcher
.on('add', handler.bind(this, 'added'))
.on('change', handler.bind(this, 'modified'))
.on('unlink', handler.bind(this, 'removed'))
// reset the error event since it got scrubbed
this._watcher.on('error', error => {
const msg ='chokidar watcher error'
log.error({error:error, msg:msg})
this.emit('error',msg,error)
})
}
async start(opts={}) {
if(this._watching) {
log.warn(`watching aleady running for ${opts.source || this.opts.source}`)
return false
}
if (!this._watcher) opts = await this._init(opts)
if (this._ready) {
this.emit('ready', true, opts)
log.trace({watching:this._watcher.getWatched(),msg:'initial files watched'})
log.info(`now watching ${opts.source || this.opts.source}`)
if (this.registerHandler(opts)) {
log.fatal('watcher was not initialzed so could not register handler')
return new Error('watcher was not initialzed so could not register handler')
}
this._watching = true
this.emit('watching',true,opts)
} else {
const msg ='watcher is not ready to start, check options and try again'
log.fatal(msg)
this.emit('error',msg)
return new Error('not ready to start check configuration options')
}
}
stop() {
if(this._watching) {
this._watching = false
this._watcher.close()
this.emit('watching',false)
}
else log.warn('not watching, nothing to stop')
}
remove() {
this.stop()
this._watcher.removeAllListeners()
delete(this._watcher)
this._ready=false
this.emit('ready',false)
}
async restart(opts,force) {
if (typeof opts ==='boolean') {force=opts,opts={}}
this.stop()
await this.start(opts)
}
getWatcher() {
return this._watcher
}
// private methods
async _init(opts={}) {
if (!opts.overwrite) {
await this._fetchIgnoreLists(this.opts.excludeFrom)
await this._fetchIgnoreLists(this.opts.ignoreList)
}
await this._fetchIgnoreLists(opts.excludeFrom,opts.overwrite)
await this._fetchIgnoreLists(opts.ignoreList,opts.overwrite)
if (!opts.ignored) opts.ignored = []
opts.ignored = Array.isArray(opts.ignored) ? opts.ignored : opts.ignored.split(',')
opts.ignored = opts.overwrite ? opts.ignored : [...this._ignored,...opts.ignored]
opts = Object.assign({},this.opts,opts) // now that ingnore arrays are dealt with merge options
return new Promise(async (resolve, reject) => {
log.debug({options:opts, msg:'intializing watch with options'})
if (opts.source) {
// create chokidar watcher
this._watcher = watch(opts.source,opts)
this._watcher.once('error', error => {
log.error({error:error, msg:'Watcher error'})
reject({error:error, msg:'Watcher error'})
})
this._watcher.once('ready', () => {
clearTimeout(readyTimeout)
log.info('initial scan sucessful, ready to start')
this._ready=true
resolve(opts)
})
log.debug(`initial scanning, timeout in ${this.timeout}ms`)
let readyTimeout = setTimeout(() =>{
log.fatal({options:opts, timeout:this.timeout, msg:'Timeout: unable to complete initial scan'})
reject('timeout during initial scan, maybe increase ready timeout')
},this.timeout)
}
else {
log.fatal('MUST provide a source directory(s) option to watch')
reject('watching - no source provided')
}
})
}
async _fetchIgnoreLists(lists,overwrite) {
if (typeof lists === 'string') lists=[lists]
if (!Array.isArray(lists)) return // no lists
let ignored = await readIgnoreLists(lists)
this._ignored = overwrite ? ignored : [...this._ignored,...ignored]
}
} //end class
// default handler
function _handler (type, f) {
log.debug(`file ${f} was ${type}`)
const fname = path.basename(f)
if ( fname.toLowerCase() === 'package.json' && path.dirname(f)=== this.opts.source.replace(/\/$/, ''))
if (type !=='modified') {
const msg = `a package.json in root of ${this.opts.source} was added or removed`
log.warning(msg)
this.emit('warning',f,msg)
return
} else{
this.emit('install', f)
}
// user might want to run debounce on the listener for this event
log.debug({file:f, type:type, msg:'file system changed, emitting'})
this.emit('changed', f,type)
} // end handler
export default Watcher
export { Watcher }