improved refactored and added in debounce option
added registerHandler Method
worked on readme
master
Kebler Network System Administrator 2021-04-08 15:08:30 -07:00
parent a47e025f17
commit 401b3c494c
5 changed files with 265 additions and 96 deletions

View File

@ -2,3 +2,5 @@ tests/
test/
*.test.js
testing/
example/
nodeman.json

View File

@ -1,23 +1,57 @@
import Watcher from '../src/watcher'
import onDeath from 'ondeath'
const USE_CUSTOM_HANDLER=true
const DEBOUNCE=0
const READY_TIMEOUT=null
;
(async () => {
let options = {
source:'./example/repo/**',
ignored:['**/dontwatch.js'],
ignoreList:['./example/repo/.gitignore'],
readyTimeout:null}
// let options = {source:'./readme.md'}
// let options = {source:'./example/*'}
debounce: DEBOUNCE,
readyTimeout:READY_TIMEOUT
}
let watcher = new Watcher(options)
await watcher.start()
watcher.on('changed',
(change) => {
console.log(`======= file ${change.file} was ${change.type} ==========`)
watcher.on('ready', (state,opts) =>{
console.log('watched files indexed and ready??',state)
if (opts) console.dir(opts)
})
watcher.on('watching', (state, opts) =>{
console.log('watcher is active and listening for changes?',state)
if (opts) console.dir(opts)
})
await watcher.start(
// {ignored:'**/another'}
)
if (USE_CUSTOM_HANDLER) {
watcher.registerHandler(
function handler (type, f) {
this.emit('custom', f, type)
} // end handler
)
watcher.on('custom',
(file, type) => {
console.log(`custom handler ======= file ${file} was ${type} ==========`)
})
}
// default handler event is `changed`
else watcher.on('changed',
(file,type) => {
console.log(`======= file ${file} was ${type} ==========`)
})
onDeath( () => {
console.log('\nHe\'s dead Jim')
watcher.remove()

View File

@ -1,10 +1,9 @@
{
"name": "@uci-utils/watcher",
"version": "0.4.1",
"version": "0.5.4",
"description": "File System Watcher Class that emits events",
"main": "src/watcher.js",
"scripts": {
"example": "node -r esm ./example/example.js",
"exampled": "UCI_ENV=dev UCI_LOG_LEVEL=debug ./node_modules/.bin/nodemon -r esm ./example/example.js",
"test": "./node_modules/.bin/mocha -r esm --timeout 30000",
"testd": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000' || exit 0",
@ -32,7 +31,8 @@
"dependencies": {
"@uci-utils/logger": "0.0.18",
"@uci-utils/read-lines": "^0.2.2",
"chokidar": "^3.5.1"
"chokidar": "^3.5.1",
"debounce-fn": "^4.0.0"
},
"devDependencies": {
"chai": "^4.3.3",

View File

@ -1,4 +1,80 @@
### File System Watcher Class
#### a uCOMmandIt Utiltiy Function
# A File System Watcher Class
**a uCOMmandIt Utility Package**
Extends Choikar
## Descripton
An extended emitter class that includes a fileystem watcher (choikar).
## Use
import and call the create function with your options or import the Watcher class extend if desired and instantiate with your options
call the async start method and you are up and running
### options
```javascript
opts = {
source:'./example/repo/**', // what to watch - required!, can be string or array of strings
ignored:['**/dontwatch.js'], // individual globs
ignoreList:['./example/repo/.gitignore'], // files containing newline lists of globs
excludeFrom: ['./example/repo/.gitignore'], // optional alternative ignore list property compatible with rsync
debounce: 0, // default is no debounce
readyTimeout:10000, // default is 10 seconds to index the files to be watched,
delDir: false, // default: ignore directory deletion events
addDir: false, // default: ignore directory addition events
unlinkDir:true // make chokidar emit on deleted directory as well.
}
```
**plus any options supported by chokidar**
[chokidar options](https://github.com/paulmillr/chokidar#persistence)
## API
### methods
```
registerHandler (func,opts) // can be called at any time but unexpected problems may arise if called when watcher is already async start(opts) // starts the watcher which will initializes the files indexing and set the handler function
stop() // stops
remove() completly removes the current watcher.
async restart(opts,force) // first stops the watcher then calls start, if force is true it will reintialize the watcher as well
getWatcher // returns handle to choikar watcher for choikar methods access
```
### getters
watching
ready
### events
changed: filepath, type // when a watched file has a change (type will be added,removed,or modified)
error: message,error
watching: state,options // listening for changes
ready: state, options // done indexing files to be watched
### handler
#### default
the built in default handler is probably sufficient for most use cases. It emits a 'changed' event with the file or directory name changed and with a type, that being modified, deleted, added. The default handler also emits a 'install' event if the file is package.json in the root of the source is modified (used for restarting), or a warning if it is deleted or added
```javascript
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
```
#### custom
one can use a custom handler by passing such a function in options or using registerHandler. The handler is passed two arguments the type of the change and the file changed.

View File

@ -3,39 +3,141 @@ 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 ignores from '@uci-utils/read-lines'
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 = Object.hasOwnProperty(opts.unlinkDir) ? opts.unlinkDir : true
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 = []
this._ignored = opts.ignored ? (Array.isArray(opts.ignored) ? opts.ignored : opts.ignored.split(',')) : []
this.handler = opts.handler || _handler
this._ready=false
this.watching=false
this._watching=false
return this
}
async init(opts) {
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) => {
opts = opts || this.opts
log.debug({options:opts, msg:'intializing watch with options'})
if (opts.excludeFrom) await this.getIgnoreLists(opts.excludeFrom)
if (opts.ignoreList) await this.getIgnoreLists(opts.ignoreList)
if (opts.source) {
opts.ignored = opts.ignored ? [...this._ignored,...opts.ignored] : this._ignored
log.debug({ignored:opts.ignored, msg:'all ignores'})
// create chokidar watcher
this._watcher = watch(opts.source,opts)
this._watcher.on('error', error => {
this._watcher.once('error', error => {
log.error({error:error, msg:'Watcher error'})
reject({error:error, msg:'Watcher error'})
})
@ -43,13 +145,12 @@ class Watcher extends Emitter {
clearTimeout(readyTimeout)
log.info('initial scan sucessful, ready to start')
this._ready=true
this.opts = opts // save options
resolve()
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')
reject('timeout during initial scan, maybe increase ready timeout')
},this.timeout)
}
else {
@ -59,78 +160,34 @@ class Watcher extends Emitter {
})
}
async start(opts) {
if(this.watching) {
log.warn(`watching aleady running for ${this.opts.source}`)
return false
}
if (!this._watcher) await this.init(opts)
if (this._ready) {
log.trace({watching:this._watcher.getWatched(),msg:'initial files watched'})
log.info(`now watching ${this.opts.source}`)
this._watcher.removeAllListeners() // just in case
this.watching = true
// define command listen handler
const handler =
(type, f) => {
log.debug(`file ${f} was ${type}`)
// convert this to a plugin/hook so it's not specific
const fname = path.basename(f)
if ( fname.toLowerCase() === 'package.json')
if (type !=='modified') {
this.emit('error',new Error('package.json was added or removed, ignoring sync and reinstall'))
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', {file:f, type:type})
} // end handler
this._watcher
.on('add', handler.bind(this, 'added'))
.on('change', handler.bind(this, 'modified'))
.on('unlink', handler.bind(this, 'removed'))
if(this.opts.unlinkDir) this._watcher.on('unlinkDir', handler.bind(this, 'dir-deleted'))
if(this.opts.addDir) this._watcher.on('addDir', handler.bind(this, 'dir-added'))
} else {
log.warn('watcher is not ready to start, check options and try again')
return new Error('not ready to start check configuration')
}
}
stop() {
if(this.watching) {
this.watching = false
this._watcher.close()
}
else log.warn('not watching, nothing to close')
}
remove() {
this.stop()
delete(this._watcher)
this._ready=false
}
async restart(opts) {
this.remove()
await this.start(opts)
}
async getIgnoreLists(lists) {
async _fetchIgnoreLists(lists,overwrite) {
if (typeof lists === 'string') lists=[lists]
let ignored = await ignores(lists)
this._ignored = [...this._ignored,...ignored]
if (!Array.isArray(lists)) return // no lists
let ignored = await readIgnoreLists(lists)
this._ignored = overwrite ? ignored : [...this._ignored,...ignored]
}
addIgnore(ignore) {
Array.isArray(ignore) ? this._ignored.push(...ignore) : this._ignored.push(ignore)
}
clearAllIgnore() { this._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 }