-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rework watcher, and pull broccoli-sane-watcher functionality into core
- Loading branch information
Showing
10 changed files
with
204 additions
and
417 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,111 +1,134 @@ | ||
'use strict' | ||
|
||
var RSVP = require('rsvp') | ||
var WatcherAdapter = require('./watcher_adapter') | ||
var logger = require('heimdalljs-logger')('broccoli:watcher') | ||
|
||
var helpers = require('broccoli-kitchen-sink-helpers') | ||
|
||
// This Watcher handles all the Broccoli logic, such as debouncing. The | ||
// WatcherAdapter handles I/O via the sane package, and could be pluggable in | ||
// principle. | ||
|
||
module.exports = Watcher | ||
function Watcher(builder, options) { | ||
this.builder = builder | ||
this.options = options || {} | ||
this.treeHashes = [] | ||
this.quitting = false | ||
this.initialBuildStarted = false | ||
this.watchDeferred = null | ||
if (this.options.debounce == null) this.options.debounce = 100 | ||
this.builder = builder | ||
this.watcherAdapter = new WatcherAdapter(this.options.saneOptions) | ||
this.currentBuild = null | ||
this._rebuildScheduled = false | ||
this._ready = false | ||
this._quitting = false | ||
this._lifetimeDeferred = null | ||
} | ||
|
||
RSVP.EventTarget.mixin(Watcher.prototype) | ||
|
||
Watcher.prototype.watch = function() { | ||
Watcher.prototype.start = function() { | ||
var self = this | ||
|
||
if (this.watchDeferred != null) throw new Error('watcher.watch() must only be called once') | ||
this.watchDeferred = RSVP.defer() | ||
self.check() | ||
return this.watchDeferred.promise | ||
if (this._lifetimeDeferred != null) throw new Error('Watcher.prototype.start() must not be called more than once') | ||
this._lifetimeDeferred = RSVP.defer() | ||
|
||
this.watcherAdapter.on('change', this._change.bind(this)) | ||
this.watcherAdapter.on('error', this._error.bind(this)) | ||
RSVP.resolve().then(function() { | ||
return self.watcherAdapter.watch(self.builder.watchedPaths) | ||
}).then(function() { | ||
logger.debug('ready') | ||
self._ready = true | ||
self.currentBuild = self._build() | ||
}).catch(function(err) { | ||
self._error(err) | ||
}) | ||
|
||
return this._lifetimeDeferred.promise | ||
} | ||
|
||
Watcher.prototype.detectChanges = function () { | ||
var changedPaths = [] | ||
Watcher.prototype._change = function() { | ||
var self = this | ||
|
||
for (var i = 0; i < this.builder.watchedPaths.length; i++) { | ||
var hash = helpers.hashTree(this.builder.watchedPaths[i]) | ||
if (hash !== this.treeHashes[i]) { | ||
changedPaths.push(this.builder.watchedPaths[i]) | ||
this.treeHashes[i] = hash | ||
} | ||
if (!this._ready) { | ||
logger.debug('change', 'ignored: before ready') | ||
return | ||
} | ||
|
||
return changedPaths | ||
if (this._rebuildScheduled) { | ||
logger.debug('change', 'ignored: rebuild scheduled already') | ||
return | ||
} | ||
logger.debug('change') | ||
this._rebuildScheduled = true | ||
// Wait for current build, and ignore build failure | ||
RSVP.resolve(this.currentBuild).catch(function() { }).then(function() { | ||
if (self._quitting) return | ||
var buildPromise = new RSVP.Promise(function(resolve, reject) { | ||
logger.debug('debounce') | ||
self.trigger('debounce') | ||
setTimeout(resolve, self.options.debounce) | ||
}).then(function() { | ||
// Only set _rebuildScheduled to false *after* the setTimeout so that | ||
// change events during the setTimeout don't trigger a second rebuild | ||
self._rebuildScheduled = false | ||
return self._build() | ||
}) | ||
self.currentBuild = buildPromise | ||
}) | ||
} | ||
|
||
Watcher.prototype.check = function() { | ||
Watcher.prototype._build = function() { | ||
var self = this | ||
|
||
this.timeoutID = null | ||
logger.debug('buildStart') | ||
this.trigger('buildStart') | ||
var buildPromise = self.builder.build() | ||
// Trigger change/error events. Importantly, if somebody else chains to | ||
// currentBuild, their callback will come after our events have | ||
// triggered, because we registered our callback first. | ||
buildPromise.then(function() { | ||
logger.debug('buildSuccess') | ||
self.trigger('buildSuccess') | ||
}, function(err) { | ||
logger.debug('buildFailure') | ||
self.trigger('buildFailure', err) | ||
}) | ||
return buildPromise | ||
} | ||
|
||
// .check can be scheduled via setTimeout or via .then, so we cannot | ||
// just rely on clearTimeout for quitting | ||
if (this.quitting) { | ||
return | ||
} | ||
Watcher.prototype._error = function(err) { | ||
var self = this | ||
|
||
try { | ||
var changedPaths = this.detectChanges() | ||
|
||
if (changedPaths.length > 0 || !this.initialBuildStarted) { | ||
this.initialBuildStarted = true | ||
this.trigger('build') | ||
this.currentBuild = this.builder.build() | ||
this.currentBuild | ||
// Trigger change/error events. If somebody else chains to | ||
// currentBuild, their callback will come after our events have | ||
// triggered, because we registered our callback first. This is subtle | ||
// but important. | ||
.then(function() { | ||
self.trigger('change') | ||
}, function(err) { | ||
self.trigger('error', err) | ||
// Do not rethrow | ||
}) | ||
.then(function() { | ||
// Resume watching | ||
self.check() | ||
}, function(err) { | ||
// A 'change' or 'error' event handler threw an error | ||
self.watchDeferred.reject(err) | ||
}) | ||
} else { | ||
// Schedule next check in 100 milliseconds | ||
var interval = this.options.interval || 100 | ||
this.timeoutID = setTimeout(this.check.bind(this), interval) | ||
} | ||
} catch (err) { | ||
// An error occurred in this.detectChanges(); this is usually because one | ||
// of the watched source directories is missing | ||
this.watchDeferred.reject(err) | ||
} | ||
logger.debug('error', err) | ||
if (this._quitting) return | ||
this._quit().catch(function() { }).then(function() { | ||
self._lifetimeDeferred.reject(err) | ||
}) | ||
} | ||
|
||
// You typically want to call watcher.quit().then() | ||
Watcher.prototype.quit = function() { | ||
var self = this | ||
|
||
this.quitting = true | ||
if (this.timeoutID) { | ||
clearTimeout(this.timeoutID) | ||
this.timeoutID = null | ||
if (this._quitting) { | ||
logger.debug('quit', 'ignored: already quitting') | ||
return | ||
} | ||
this._quit().then(function() { | ||
self._lifetimeDeferred.resolve() | ||
}, function(err) { | ||
self._lifetimeDeferred.reject(err) | ||
}) | ||
} | ||
|
||
return RSVP.resolve(this.currentBuild) | ||
.catch(function(err) { | ||
// Ignore build errors to stop them from being propagated to | ||
// RSVP.on('error') | ||
}) | ||
.finally(function() { | ||
// It might have been rejected in the meantime, in which case this has | ||
// no effect | ||
self.watchDeferred.resolve() | ||
}) | ||
Watcher.prototype._quit = function(err) { | ||
var self = this | ||
|
||
this._quitting = true | ||
logger.debug('quitStart') | ||
|
||
return RSVP.resolve().then(function() { | ||
return self.watcherAdapter.quit() | ||
}).finally(function() { | ||
// Wait for current build, and ignore build failure | ||
return RSVP.resolve(self.currentBuild).catch(function() { }) | ||
}).finally(function() { | ||
logger.debug('quitEnd') | ||
}) | ||
} |
Oops, something went wrong.