forked from 3rd-Eden/fs.notify
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnotify.js
More file actions
278 lines (221 loc) · 6.39 KB
/
notify.js
File metadata and controls
278 lines (221 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
"use strict";
var EventEmitter = require('events').EventEmitter
, async = require('async')
, retry = require('retry')
, pathy = require('path')
, fs = require('fs');
/**
* Watch for file changes.
*
* @constructor
* @param {Array} files
*/
function Notify (files) {
this.FSWatchers = {}; // stores the watchers
this.FStats = {}; // latest file stats
this.retry = {}; // files we are retrying
this.maxRetries = 10; // amount of retries we can do per file
if (files) this.add(files);
}
Notify.prototype.__proto__ = EventEmitter.prototype;
/**
* Add files that need to be watched for changes. It filters out all non
* existing paths from the array and it currently does not give a warning for
* that. So make sure that your stuff is in place.
*
* @param {Array} files files to watch
* @api public
*/
Notify.prototype.add = function add(files) {
var self = this;
// edge case where files isn't an array
if (!Array.isArray(files)) files = [files];
// filter out any non existing files
async.filter(files, fs.exists, function (files) {
files.forEach(self.watch, self);
});
return this;
};
/**
* Close the file notifications.
*
* @api public
*/
Notify.prototype.close = function close() {
var watcher, FSWatcher;
// close all FSWatches
for (watcher in this.FSWatchers) {
FSWatcher = this.FSWatchers[watcher];
FSWatcher.removeAllListeners();
if ('close' in FSWatcher) FSWatcher.close();
}
// release the watches from memory
this.FSWatchers = {};
this.FStats = {};
this.emit('close');
return this;
};
/**
* Start watching the path for changes.
*
* @param {String} path
* @api private
*/
Notify.prototype.watch = function watch(path) {
var self = this
, FSWatcher;
// resolve the path this allows us to prevent duplicates of ./index.js and
// index.js
path = pathy.resolve(path);
// check for duplicates
if (this.FSWatchers[path]) return this;
// update the fs stat
fs.stat(path, function stats(err, stat) {
if (stat) self.FStats[path] = stat;
});
// store the file watcher and add the path to where we are watching, this does
// create a hidden class for it.. So it's a bit slower, but we need an easy
// way to find the path for the watcher
FSWatcher = this.FSWatchers[path] = fs.watch(path);
FSWatcher.path = path;
// add the FSWatcher event listeners
FSWatcher.on('change', this.change.bind(this, FSWatcher));
FSWatcher.on('error', this.error.bind(this, FSWatcher));
return this;
};
/**
* Manually search for file changes.
*
* @param {FSWatcher} FSWatcher
* @param {String} event the name of the event
* @api public
*/
Notify.prototype.manually = function manually(FSWatcher, event) {
var self = this
, files = FSWatcher && FSWatcher.path
? [FSWatcher.path]
: Object.keys(this.FStats);
// loop over the files and compare the fs.Stat's
files.forEach(function test(file) {
var current = self.FStats[file];
if (!current) return;
// make sure that the file we are going to check actually exists.. or we
// will get a failed fs.stat operation
self.ensure(file, function existance(exists) {
if (!exists) return;
fs.stat(file, function stats(err, stat) {
if (!stat || err) return;
// check if the modification time has changed
if (+current.mtime !== +stat.mtime) {
self.emit('change', file, event);
}
});
});
});
return this;
};
/**
* Re-attach the watch process.
*
* @api private
*/
Notify.prototype.reset = function reset(path) {
var self = this
, FSWatcher = this.FSWatchers[path];
// close it
if (FSWatcher) {
FSWatcher.close();
FSWatcher.removeAllListeners();
}
// clear it from our queue
delete this.FSWatchers[path];
delete this.FStats[path];
// check if we already have a retry operation running, as multiple events can
// trigger this call..
if (this.retry[path]) return this;
this.retry[path] = true;
return this.ensure(path, function existance(exists) {
delete self.retry[path];
if (!exists) {
return self.emit('error', new Error('File ' + path + ' is gone'), path);
}
self.watch(path);
});
};
/**
* Small helper function to ensure that a file available.. As IDE's could be
* writing the new result to a new file, remove the current file and put the new
* file in to place. So during that operation it could be that we do a fs.stat
* and get a missing file.
*
* It could still happen here.. but it's less likely that the file is removed
* after we have detected it's existance again.
*
* @param {String} path
* @param {Function} fn
* @api private
*/
Notify.prototype.ensure = function ensure(path, fn) {
var operation = retry.operation({
retries: this.maxRetries
, factor: 3
, minTimeout: 100
, maxTimeout: 60 * 100
, randomize: true
});
// do some fault tolerant existance checking
operation.attempt(function attempt() {
fs.exists(path, function existance(exists) {
// we are using .exists, and that doens't return an error just a boolean,
// so we need to create a fake error object for your retry operation
var fakeErr = exists ? undefined : new Error();
if (operation.retry(fakeErr)) return;
fn(exists);
});
});
return this;
};
/**
* A file change has been triggered.
*
* @param {FSWatcher} FSWatcher
* @param {String} event changed, renamed
* @param {String} filename filename of the change
* @api private
*/
Notify.prototype.change = function change(FSWatcher, event, filename) {
if (!filename) return this.manually(FSWatcher, event).reset(FSWatcher.path);
this.emit('change', filename, event, FSWatcher.path);
this.reset(FSWatcher.path);
return this;
};
/**
* Handle watching errors.
*
* @param {FSWatcher} FSWatcher
* @param {Error} err
* @api private
*/
Notify.prototype.error = function error(FSWatcher, err) {
return this.reset(FSWatcher.path);
};
// expose the notifier
module.exports = Notify;
/**
* Expose a fs.watch that doesn't suck hairy monkey balls.
*
* @param {String} file file to watch
* @param {Function} callback callback
* @api public
*/
Notify.watch = function watch(file, callback) {
var notification = new Notify([file]);
return notification.on('change', callback);
};
/**
* Expose version number.
*
* @type {String}
* @api private
*/
Notify.version = require('./package.json').version;