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/
*.test.js *.test.js
testing/ testing/
example/
nodeman.json

View File

@ -1,23 +1,57 @@
import Watcher from '../src/watcher' import Watcher from '../src/watcher'
import onDeath from 'ondeath' import onDeath from 'ondeath'
const USE_CUSTOM_HANDLER=true
const DEBOUNCE=0
const READY_TIMEOUT=null
;
(async () => { (async () => {
let options = { let options = {
source:'./example/repo/**', source:'./example/repo/**',
ignored:['**/dontwatch.js'], ignored:['**/dontwatch.js'],
ignoreList:['./example/repo/.gitignore'], ignoreList:['./example/repo/.gitignore'],
readyTimeout:null} debounce: DEBOUNCE,
// let options = {source:'./readme.md'} readyTimeout:READY_TIMEOUT
// let options = {source:'./example/*'} }
let watcher = new Watcher(options) let watcher = new Watcher(options)
await watcher.start()
watcher.on('changed', watcher.on('ready', (state,opts) =>{
(change) => { console.log('watched files indexed and ready??',state)
console.log(`======= file ${change.file} was ${change.type} ==========`) 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( () => { onDeath( () => {
console.log('\nHe\'s dead Jim') console.log('\nHe\'s dead Jim')
watcher.remove() watcher.remove()

View File

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

View File

@ -1,4 +1,80 @@
### File System Watcher Class # A File System Watcher Class
#### a uCOMmandIt Utiltiy Function **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' import path from 'path'
// third party imports // third party imports
import { watch } from 'chokidar' import { watch } from 'chokidar'
import debounce from 'debounce-fn'
// UCI imports // UCI imports
import ignores from '@uci-utils/read-lines' import readIgnoreLists from '@uci-utils/read-lines'
import logger from '@uci-utils/logger' import logger from '@uci-utils/logger'
let log = {} let log = {}
const READY_TIMEOUT = 10000 //default const READY_TIMEOUT = 10000 //default
class Watcher extends Emitter { class Watcher extends Emitter {
constructor(opts={}) { constructor(opts={}) {
super() super()
log = logger({ package:'@uci-utils/watcher', class:'Watcher', file:'src/watcher.js'}) 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.opts = opts
this.timeout = process.env.READY_TIMEOUT || opts.readyTimeout || READY_TIMEOUT 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._ready=false
this.watching=false this._watching=false
return this return this
} }
async init(opts) { get watching () {
return new Promise(async (resolve, reject) => { return this._watching
opts = opts || this.opts }
log.debug({options:opts, msg:'intializing watch with options'})
if (opts.excludeFrom) await this.getIgnoreLists(opts.excludeFrom) get ready () {
if (opts.ignoreList) await this.getIgnoreLists(opts.ignoreList) return this._ready
if (opts.source) { }
opts.ignored = opts.ignored ? [...this._ignored,...opts.ignored] : this._ignored
log.debug({ignored:opts.ignored, msg:'all ignores'}) registerHandler(func,opts) {
this._watcher = watch(opts.source,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 => { 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'}) log.error({error:error, msg:'Watcher error'})
reject({error:error, msg:'Watcher error'}) reject({error:error, msg:'Watcher error'})
}) })
@ -43,13 +145,12 @@ class Watcher extends Emitter {
clearTimeout(readyTimeout) clearTimeout(readyTimeout)
log.info('initial scan sucessful, ready to start') log.info('initial scan sucessful, ready to start')
this._ready=true this._ready=true
this.opts = opts // save options resolve(opts)
resolve()
}) })
log.debug(`initial scanning, timeout in ${this.timeout}ms`) log.debug(`initial scanning, timeout in ${this.timeout}ms`)
let readyTimeout = setTimeout(() =>{ let readyTimeout = setTimeout(() =>{
log.fatal({options:opts, timeout:this.timeout, msg:'Timeout: unable to complete initial scan'}) 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) },this.timeout)
} }
else { else {
@ -59,78 +160,34 @@ class Watcher extends Emitter {
}) })
} }
async start(opts) { async _fetchIgnoreLists(lists,overwrite) {
if(this.watching) { if (typeof lists === 'string') lists=[lists]
log.warn(`watching aleady running for ${this.opts.source}`) if (!Array.isArray(lists)) return // no lists
return false let ignored = await readIgnoreLists(lists)
this._ignored = overwrite ? ignored : [...this._ignored,...ignored]
} }
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}`) } //end class
this._watcher.removeAllListeners() // just in case
this.watching = true // default handler
// define command listen handler function _handler (type, f) {
const handler =
(type, f) => {
log.debug(`file ${f} was ${type}`) log.debug(`file ${f} was ${type}`)
// convert this to a plugin/hook so it's not specific
const fname = path.basename(f) const fname = path.basename(f)
if ( fname.toLowerCase() === 'package.json') if ( fname.toLowerCase() === 'package.json' && path.dirname(f)=== this.opts.source.replace(/\/$/, ''))
if (type !=='modified') { if (type !=='modified') {
this.emit('error',new Error('package.json was added or removed, ignoring sync and reinstall')) const msg = `a package.json in root of ${this.opts.source} was added or removed`
log.warning(msg)
this.emit('warning',f,msg)
return return
} else{ } else{
this.emit('install', f) this.emit('install', f)
} }
// user might want to run debounce on the listener for this event // user might want to run debounce on the listener for this event
log.debug({file:f, type:type, msg:'file system changed, emitting'}) log.debug({file:f, type:type, msg:'file system changed, emitting'})
this.emit('changed', {file:f, type:type}) this.emit('changed', f,type)
} // end handler } // 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) {
if (typeof lists === 'string') lists=[lists]
let ignored = await ignores(lists)
this._ignored = [...this._ignored,...ignored]
}
addIgnore(ignore) {
Array.isArray(ignore) ? this._ignored.push(...ignore) : this._ignored.push(ignore)
}
clearAllIgnore() { this._ignored = [] }
}
export default Watcher export default Watcher
export { Watcher } export { Watcher }