watchr.coffee

#

Watchr is used to be nofitied when a change happens to, or within a directory.
You will not be notified what file was changed, or how it was changed.
It will track new files and their changes too, and remove listeners for deleted files appropriatly.

The source code here is written as an experiment of literate programming
Which means you would be able to understand it, without knowing code

#

Require the file system module for node.js This provides us with what we need to interact with the file system (aka files and directories)

fs = require('fs')

Let's set the debugging mode We will use this later on when outputting messages that make our code easier to debug Note: when we publish the module, we want to set this as off, as we don't want the application using us to spurt our all our debug messages!

debug = false

Now to make watching files more convient and managed, we'll create a class which we can use to attach to each file It'll provide us with the API and abstraction we need to accomplish difficult things like recursion We'll also store a global store of all the watchers and their paths so we don't have multiple watchers going at the same time for the same file - as that would be quite ineffecient

watchers = {}
Watcher = class

The path this class instance is attached to

	path: null

Is it a directory or not?

	isDirectory: null

Our fs.stat object, it contains things like change times, size, and is it a directory

	stat: null

The events that we will trigger when we detect a change We make this as an array as otherwise we would have to have one listener for every event as that would be quite slow So instead we have one listener, with many events

	events: []

The node.js file watcher instance, we have to open and close this, it is what notifies us of the events

	fswatcher: null

We also want to setup a delay between change events, as if we changed a lot of files, we just want to be notified once, instead of like 1000 times

	timeout: null
	delay: 500

The watchers for the children of this watcher will go here This is for when we are watching a directory, we will scan the directory and children go here

	children: []

We have to store the current state of the watcher and it is asynchronous (things can fire in any order) as such, we don't want to be doing particular things if this watcher is deactivated (closed)

	state: 'pending'

The method we will use to watch the files Preferably we use watchFile, however we may need to use watch in case watchFile doesn't exist (e.g. windows)

	method: null
	

Now it's time to construct our watcher We give it a path, and give it some events to use Then we get to work with watching it

	constructor: (path,events=[]) ->
		@children = []
		@events = []
		@path = path
		@addEvents events
		fs.stat @path, (err,stat) =>
			return  if @state is 'closed'
			throw err  if err
			@stat = stat
			@isDirectory = stat.isDirectory()
			@watch()
	

Let's now add our events We should support being passed a list of events, as well as one event, or no events (for error prevention) We should also not add an event if it is already added

	addEvents: (events) ->
		unless events instanceof Array
			if events
				events = [events]
			else
				events = []
		for event in events
			found = false
			for storedEvent in @events
				if storedEvent is event
					found = true
					break
			if not found
				@events.push(event)
		@
	

Before we start watching, we'll have to setup the functions our watcher will need

It will need function to trigger all of our watcher's events

	trigger: ->
		console.log "trigger: #{@path}"  if debug
		for event in @events
			event()
		@
	

We also need something so when a file is changed, we wait a while until things have calmed down and once they have calmed down, then trigger our events

	changed: ->
		console.log "changed: #{@path}"  if debug
		if @timeout
			clearTimeout(@timeout)
			@timeout = false
		@timeout = setTimeout(
			=> @trigger()
			@delay
		)
		@
	

We will need something to close our listener for removed or renamed files As renamed files are a bit difficult we will want to close and delete all the watchers for all our children too Essentially it is like a self-destruct without the body parts

	close: ->
		return @  if @state is 'closed'
		console.log "close: #{@path}"  if debug

Close and destroy children

		for watcher,index in @children
			watcher.close()
			delete @children[index]
		@children = []

Close ourself

		if @state isnt 'closed'

Close listener

			if @method is 'watchFile'
				fs.unwatchFile @path
			else if @method is 'watch'  and  @fswatcher
				@fswatcher.close()
				@fswatcher = null
				

Updated state

			@state = 'closed'

Delete our watchers reference

		delete watchers[@path]  if watchers[@path]?

Chain

		@
	

We need something to figure out what to do when a file is changed It will check if we are still active, and if so, then Handle the events appropriatly

	handler: (action) ->

Ignore if we are closed

		return  if @state is 'closed'
		

Using fs.watchFile

		if arguments.length is 2  and  typeof arguments[0] isnt 'string'
			console.log "watchFile: #{@path}"  if debug
			curr = arguments[0]
			prev = arguments[1]
			console.log arguments  if debug
			return @  if curr.mtime.getTime() is prev.mtime.getTime()  or  curr.size is prev.size
			@changed()
			@watch()
			return @
		

Using fs.watch

		console.log "#{action}: #{@path}"  if debug

Renames and new files If we are a file then stop our close our watcher, as an event will also have fired for the parent directory If we are the parent directory, then trigger our change event, then re-initialise all our listernes, as we want to close listerners for deleted files and add new listeners for added files

		if action is 'rename'
			if @isDirectory is false
				@close()
			else
				@changed()
				try
					@watch()
			

Changed files If we were a change, then let's check that something did actually change If it did, then trigger our change event

		else if action is 'change'
			fs.stat @path, (err,stat) =>

Ignore if we are closed

				return  if @state is 'closed'
				throw err  if err
				return  if stat.mtime.getTime() is @stat.mtime.getTime()  and  stat.size is @stat.size
				@stat = stat
				@changed()
		

Chain

		@
	

Setup the watching for our path If we are already watching this path then let's start again (call close) Then if we are a directory, let's recurse Finally, let's initialise our node.js watcher that'll let us know when things happen and update our state to active

	watch: ->
		@close()
		console.log "watch: #{@path}"  if debug
		if @isDirectory
			fs.readdir @path, (err,files) =>
				throw err  if err
				for file in files
					continue  if /^[\.~]/.test file
					filePath = @path+'/'+file
					watcher = watch filePath, => @changed()
					@children.push watcher
		try
			fs.watchFile @path, => @handler.apply(@,arguments)
			@method = 'watchFile'
		catch err
			@fswatcher = fs.watch @path, => @handler.apply(@,arguments)
			@method = 'watch'
		@state = 'active'
		@

Provide our interface to the applications that use watchr This will create our new Watcher class for the path we want (or use an existing one, and add the events) Watcher also uses this too

watch = (path,events) ->

Check if we are already watching that path

	if watchers[path]?

We do, so let's use that one instead

		watchers[path].addEvents(events)
	else

We don't, so let's create a new one

		watchers[path] = new Watcher(path,events)

Now let's provide node.js with our public API In other words, what the application that calls us has access to

module.exports = {watch}