'use strict'; var crypto = require('crypto'); var objectAssign = require('object-assign'); var objectOmit = require('object.omit'); var Bluebird = require('bluebird'); var tryJsonParse = require('try-json-parse'); var TaskProxy = function(opts) { objectAssign(this, { task: opts.task, file: opts.file, opts: opts.opts, originalPath: opts.file.path }); }; function makeHash(key) { return crypto.createHash('md5').update(key).digest('hex'); } objectAssign(TaskProxy.prototype, { processFile: function() { var self = this; return this._checkForCachedValue().then(function(cached) { // If we found a cached value // The path of the cache key should also be identical to the original one when the file path changed inside the task if (cached.value && (!cached.value.filePathChangedInsideTask || cached.value.originalPath === self.file.path)) { // Extend the cached value onto the file, but don't overwrite original path info var file = objectAssign( self.file, objectOmit(cached.value, ['cwd', 'path', 'base', 'stat', 'history']) ); // Restore the file path if it was set if (cached.value.path && cached.value.filePathChangedInsideTask) { file.path = cached.value.path; } return file; } // Otherwise, run the proxied task return self._runProxiedTaskAndCache(cached.key); }); }, removeCachedResult: function() { var self = this; return this._getFileKey().then(function(cachedKey) { var removeCached = Bluebird.promisify(self.opts.fileCache.removeCached, { context: self.opts.fileCache }); return removeCached(self.opts.name, cachedKey); }); }, _getFileKey: function() { var getKey = this.opts.key; if (typeof getKey === 'function' && getKey.length === 2) { getKey = Bluebird.promisify(getKey.bind(this.opts)); } return Bluebird.resolve(getKey(this.file)).then(function(key) { if (!key) { return key; } return makeHash(key); }); }, _checkForCachedValue: function() { var self = this; return this._getFileKey().then(function(key) { // If no key returned, bug out early if (!key) { return { key: key, value: null }; } var getCached = Bluebird.promisify(self.opts.fileCache.getCached.bind(self.opts.fileCache)); return getCached(self.opts.name, key).then(function(cached) { if (!cached) { return { key: key, value: null }; } var parsedContents = tryJsonParse(cached.contents); if (parsedContents === undefined) { parsedContents = {cached: cached.contents}; } if (self.opts.restore) { parsedContents = self.opts.restore(parsedContents); } return { key: key, value: parsedContents }; }); }); }, _runProxiedTaskAndCache: function(cachedKey) { var self = this; return self._runProxiedTask().then(function(result) { // If this wasn't a success, continue to next task // TODO: Should this also offer an async option? if (self.opts.success !== true && !self.opts.success(result)) { return result; } return self._storeCachedResult(cachedKey, result).then(function() { return result; }); }); }, _runProxiedTask: function() { var self = this; return new Bluebird(function(resolve, reject) { function handleError(err) { // TODO: Errors will step on each other here // Reduce the maxListeners back down self.task.setMaxListeners(self.task._maxListeners - 1); reject(err); } function handleData(datum) { // Wait for data (can be out of order, so check for matching file we wrote) if (self.file !== datum) { return; } // Be good citizens and remove our listeners self.task.removeListener('error', handleError); self.task.removeListener('data', handleData); // Reduce the maxListeners back down self.task.setMaxListeners(self.task._maxListeners - 2); resolve(datum); } // Bump up max listeners to prevent memory leak warnings var currMaxListeners = self.task._maxListeners || 0; self.task.setMaxListeners(currMaxListeners + 2); self.task.on('data', handleData); self.task.once('error', handleError); // Run through the other task and grab output (or error) // Not sure if a _.defer is necessary here self.task.write(self.file); }); }, _getValueFromResult: function(result) { var getValue; if (typeof this.opts.value !== 'function') { if (typeof this.opts.value === 'string') { getValue = {}; getValue[this.opts.value] = result[this.opts.value]; } return Bluebird.resolve(getValue); } else if (this.opts.value.length === 2) { // Promisify if passed a node style function getValue = Bluebird.promisify(this.opts.value.bind(this.opts)); } else { getValue = this.opts.value; } return Bluebird.resolve(getValue(result)); }, _storeCachedResult: function(key, result) { var self = this; // If we didn't have a cachedKey, skip caching result if (!key) { return Bluebird.resolve(result); } return this._getValueFromResult(result).then(function(value) { var val; var addCached = Bluebird.promisify(self.opts.fileCache.addCached.bind(self.opts.fileCache)); if (typeof value !== 'string') { if (value && typeof value === 'object' && Buffer.isBuffer(value.contents)) { // Shallow copy so "contents" can be safely modified val = objectAssign({}, value); val.contents = val.contents.toString('utf8'); } // Check if the task changed the file path if (value.path !== self.originalPath) { value.filePathChangedInsideTask = true; } // Keep track of the original path value.originalPath = self.originalPath; val = JSON.stringify(value, null, 2); } else { val = value; } return addCached(self.opts.name, key, val); }); } }); module.exports = TaskProxy;