{EventEmitter} = require 'events'
fs = require 'fs'
path = require 'path'{EventEmitter} = require 'events'
fs = require 'fs'
path = require 'path'Options:
retain means that if something is later created at the same location
as the target, the new entity will be watched.debounce means that changes that occur within 1 second of each other
will be treated as a single change. This also allows "echo" events that
occur under OS X to be ignored.include means that if the target is a directory, files contained in that
directory will be treated like targets. (Otherwise, directory events will
be forwarded directly from fs.watch.)recurse means that if the target is a directory, all of its
subdirectories will also be counted as targets.persistent is identical to fs.watch's persistent option. If
disabled, the process may exit while files are being watched.ignore could contain RegExp pattern or function against which
added files will be tested.defaults =
retain: false
debounce: false
include: false
recurse: false
persistent: true
ignored: nullwatchit = (target, options, callback) ->The options argument and the callback are both optional
if typeof options is 'function'
callback = options
options = {}
options = extend {}, defaults, options ? {}Generic function that will check if some file is ignored.
ignored = (file) ->
if options.ignored
if typeof options.ignored.test is 'function'
options.ignored.test(file)
else
options.ignored(file)
else
noemitter will be returned from the function; it emits "change", "create",
and "unlink" events. It also emits "success" and "failure" events the
first time a target is found or not found, respectively.
emitter = options.emitter = options.emitter ? new WatchitEmitter(callback)emitter also keeps track of the mtime on each target. If a target is
already being watched on the same emitter, we return null rather than
watch the same target multiple times.
emitter.targets ?= {}
return null if emitter.targets[target]The emitter can also be used to stop the watching process. Because the
same emitter is used for directory children (if include or recurse is
enabled), a single "close" event can shut down several fswatchers.
fswatcher = null
emitter.close = -> emitter.emit 'close', target
emitter.on 'close', -> fswatcher?.close()Start watching
do watchTarget = ->
emitter.targets[target] = {}
fs.stat target, (err, stats) ->
fail = (err) ->
if options.retain
notifyWhenExists target, ->
emitter.emit 'create', target
watchTarget()
else
emitter.emit 'failure', target, err
return fail err if err
emitter.targets[target].stats = stats
try
if stats.isDirectory()
fswatcher = watchTargetDir()
scanTargetDir true if options.include or options.recurse
else
fswatcher = watchTargetFile()
catch e
return fail e
emitter.emit 'success', target
fswatcher.on 'error', (err) -> throw errIf the target is lost and retain is enabled, we watchTarget again
retainTarget = watchTargetIf the target is lost, we close the FSWatcher
unwatchTarget = ->
fswatcher.close()
delete emitter.targets[target]
watchTargetFile = ->
fs.watch target, {persistent: options.persistent}, (event) ->
if event is 'rename'Has the target been unlinked, or merely replaced?
fs.stat target, (err) ->
if err
unwatchTarget()
retainTarget() if options.retainTODO: Distinguish renames from unlinks, somehow
emitter.emit 'unlink', target
else
emitter.emit 'change', target
else if event is 'change'
if options.debounce
conditionalTimeout target, 1000, ->
fs.stat target, (err, stats) ->
return if err or target not of emitter.targets
prevStats = emitter.targets[target].stats
return if stats.mtime.getTime() is prevStats.mtime.getTime()
emitter.targets[target].stats = stats
emitter.emit 'change', target
else
emitter.emit 'change', target
watchTargetDir = ->
fs.watch target, {persistent: options.persistent}, (event, filename) ->
if event is 'rename'Is this happening to the target, or one of its children?
fs.stat target, (err) ->
if err
unwatchTarget()
retainTarget() if options.retain
emitter.emit 'unlink', target
else
emitter.emit 'rename', target unless options.include
scanTargetDir() if options.include or options.recurse
else
throw new Error "Unexpected directory event: #{event}"
scanTargetDir = (initial) ->
fs.readdir target, (err, items) ->
return if err
for item in items
do (item) ->
return if ignored item
itemPath = path.join(target, item)
fs.stat itemPath, (err, stats) ->
return if err
isDir = stats.isDirectory()
if (isDir and options.recurse) or (!isDir and options.include)watchit returns null if target is already watched
if watchit itemPath, extend({emitter}, options)
emitter.emit 'create', itemPath unless initial
emitterclass WatchitEmitter extends EventEmitter
constructor: (@callback) ->
emit: (event, filename, etc...) ->
return if event is 'newListener'
super event, filename, etc...
super 'all', event, filename, etc...
@callback? event, filename, etc...
extend = (obj, sources...) ->
for source in sources
for prop of source
obj[prop] = source[prop] if prop of source
objSet a timeout, unless a timeout with the same key already exists.
pendingTimeouts = {}
conditionalTimeout = (key, time, callback) ->
return if key of pendingTimeouts
pendingTimeouts[key] = 1
setTimeout (->
delete pendingTimeouts[key]
callback()
), timeTo be notified when target does not exist, we must watch its parent.
notifyWhenExists = (target, callback) ->
throw new Error 'notifyWhenExists requires a callback' unless callback
parentDir = path.join target, '..'And if parentDir does not exist, we must watch its parent. And so on...
levelUp = ->
notifyWhenExists parentDir, ->
notifyWhenExists target, callback
fs.exists target, (exists) ->
return callback() if exists
try
fswatcher = fs.watch parentDir, {persistent: options.persistent}, ->
fs.readdir parentDir, (err, items) ->
if errparentDir no longer exists
fswatcher.close()
levelUp()
return
for item in items
if path.join(parentDir, item) is targetOur target now exists
fswatcher.close()
return callback()
return
catch e
levelUp()While Watchit's main export is its titular function, functions which can be tested independently are attached.
module.exports = watchit
module.exports.conditionalTimeout = conditionalTimeout
module.exports.notifyWhenExists = notifyWhenExists