0.5.4
improved refactored and added in debounce option added registerHandler Method worked on readmemaster
parent
a47e025f17
commit
401b3c494c
|
@ -2,3 +2,5 @@ tests/
|
||||||
test/
|
test/
|
||||||
*.test.js
|
*.test.js
|
||||||
testing/
|
testing/
|
||||||
|
example/
|
||||||
|
nodeman.json
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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",
|
||||||
|
|
82
readme.md
82
readme.md
|
@ -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.
|
||||||
|
|
217
src/watcher.js
217
src/watcher.js
|
@ -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 }
|
||||||
|
|
Loading…
Reference in New Issue