UNPKG

8 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5const path_1 = __importDefault(require("path"));
6const events_1 = require("events");
7const watcher_adapter_1 = __importDefault(require("./watcher_adapter"));
8const logger = require('heimdalljs-logger')('broccoli:watcher');
9// This Watcher handles all the Broccoli logic, such as debouncing. The
10// WatcherAdapter handles I/O via the sane package, and could be pluggable in
11// principle.
12class Watcher extends events_1.EventEmitter {
13 constructor(builder, watchedNodes, options = {}) {
14 super();
15 this.options = options;
16 if (this.options.debounce == null) {
17 this.options.debounce = 100;
18 }
19 this._currentBuild = Promise.resolve();
20 this.builder = builder;
21 this.watcherAdapter =
22 this.options.watcherAdapter || new watcher_adapter_1.default(watchedNodes, this.options.saneOptions);
23 this._rebuildScheduled = false;
24 this._ready = false;
25 this._quittingPromise = null;
26 this._lifetime = null;
27 this._changedFiles = [];
28 }
29 get currentBuild() {
30 return this._currentBuild;
31 }
32 // TODO: this is an interim solution, pending a largerly cleanup of this class.
33 // Currently I can rely on understanding the various pieces of this class, to
34 // know this is safe. This is not a good long term solution, but given
35 // relatively little time to address this currently, it is "ok". I do plan,
36 // as time permits to circle back, and do a more thorough refactoring of this
37 // class, to ensure it is safe for future travelers.
38 _updateCurrentBuild(promise) {
39 this._currentBuild = promise;
40 promise.catch(() => {
41 /**
42 * The watcher internally follows currentBuild, and outputs errors appropriately.
43 * Since watcher.currentBuild is public API, we must allow public follows
44 * to still be informed of rejections. However we do not want `_currentBuild` itself
45 * to trigger unhandled rejections.
46 *
47 * By catching errors here, but returning `promise` instead of the chain from
48 * `promise.catch`, both goals are accomplished.
49 */
50 });
51 }
52 start() {
53 if (this._lifetime != null) {
54 throw new Error('Watcher.prototype.start() must not be called more than once');
55 }
56 this._lifetime = {};
57 let lifetime = this._lifetime;
58 lifetime.promise = new Promise((resolve, reject) => {
59 lifetime.resolve = resolve;
60 lifetime.reject = reject;
61 });
62 this.watcherAdapter.on('change', this._change.bind(this));
63 this.watcherAdapter.on('error', this._error.bind(this));
64 this._updateCurrentBuild((async () => {
65 try {
66 await this.watcherAdapter.watch();
67 logger.debug('ready');
68 this._ready = true;
69 }
70 catch (e) {
71 this._error(e);
72 }
73 await this._build();
74 })());
75 return this._lifetime.promise;
76 }
77 async _change(event, filePath, root) {
78 this._changedFiles.push(path_1.default.join(root, filePath));
79 if (!this._ready) {
80 logger.debug('change', 'ignored: before ready');
81 return;
82 }
83 if (this._rebuildScheduled) {
84 logger.debug('change', 'ignored: rebuild scheduled already');
85 return;
86 }
87 logger.debug('change', event, filePath, root);
88 this.emit('change', event, filePath, root);
89 this._rebuildScheduled = true;
90 try {
91 // Wait for current build, and ignore build failure
92 await this.currentBuild;
93 }
94 catch (e) {
95 /* we don't care about failures in the last build, simply start the
96 * next build once the last build has completed
97 * */
98 }
99 if (this._quitting) {
100 this._updateCurrentBuild(Promise.resolve());
101 return;
102 }
103 this._updateCurrentBuild(new Promise((resolve, reject) => {
104 logger.debug('debounce');
105 this.emit('debounce');
106 setTimeout(() => {
107 // Only set _rebuildScheduled to false *after* the setTimeout so that
108 // change events during the setTimeout don't trigger a second rebuild
109 try {
110 this._rebuildScheduled = false;
111 resolve(this._build(path_1.default.join(root, filePath)));
112 }
113 catch (e) {
114 reject(e);
115 }
116 }, this.options.debounce);
117 }));
118 }
119 _build(filePath) {
120 logger.debug('buildStart');
121 this.emit('buildStart');
122 const start = process.hrtime();
123 // This is to maintain backwards compatibility with broccoli-sane-watcher
124 let annotation = {
125 type: filePath ? 'rebuild' : 'initial',
126 reason: 'watcher',
127 primaryFile: filePath,
128 changedFiles: this._changedFiles,
129 };
130 const buildPromise = this.builder.build(null, annotation);
131 // Trigger change/error events. Importantly, if somebody else chains to
132 // currentBuild, their callback will come after our events have
133 // triggered, because we registered our callback first.
134 buildPromise.then((results = {}) => {
135 const end = process.hrtime(start);
136 logger.debug('Build execution time: %ds %dms', end[0], Math.round(end[1] / 1e6));
137 logger.debug('buildSuccess');
138 // This property is added to keep compatibility for ember-cli
139 // as it relied on broccoli-sane-watcher to add it:
140 // https://github.com/ember-cli/broccoli-sane-watcher/blob/48860/index.js#L92-L95
141 //
142 // This is "undefined" during the initial build.
143 results.filePath = filePath;
144 this._changedFiles = [];
145 this.emit('buildSuccess', results);
146 }, (err) => {
147 this._changedFiles = [];
148 logger.debug('buildFailure');
149 this.emit('buildFailure', err);
150 });
151 return buildPromise;
152 }
153 async _error(err) {
154 if (this._quittingPromise) {
155 logger.debug('error', 'ignored: already quitting');
156 return this._quittingPromise;
157 }
158 logger.debug('error', err);
159 this.emit('error', err);
160 try {
161 await this._quit();
162 }
163 catch (e) {
164 // ignore errors that occur during quitting
165 }
166 if (this._lifetime && typeof this._lifetime.reject === 'function') {
167 this._lifetime.reject(err);
168 }
169 }
170 quit() {
171 if (this._quittingPromise) {
172 logger.debug('quit', 'ignored: already quitting');
173 return this._quittingPromise;
174 }
175 let quitting = this._quit();
176 if (this._lifetime && typeof this._lifetime.resolve === 'function') {
177 this._lifetime.resolve(quitting);
178 return this._lifetime.promise;
179 }
180 else {
181 return quitting;
182 }
183 }
184 _quit() {
185 logger.debug('quitStart');
186 this.emit('quitStart');
187 this._quittingPromise = (async () => {
188 try {
189 await this.watcherAdapter.quit();
190 }
191 finally {
192 try {
193 await this.currentBuild;
194 }
195 catch (e) {
196 // Wait for current build, and ignore build failure
197 }
198 logger.debug('quitEnd');
199 this.emit('quitEnd');
200 }
201 })();
202 return this._quittingPromise;
203 }
204}
205module.exports = Watcher;
206//# sourceMappingURL=watcher.js.map
\No newline at end of file