Skip to content

Commit

Permalink
Merge pull request #870 from codingtwinky/promisify_cache
Browse files Browse the repository at this point in the history
Promisify cache.js
  • Loading branch information
jung-kim authored Feb 22, 2017
2 parents d98827b + e167c8e commit 20d1c95
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 142 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
Use the following format for additions: ` - VERSION: [feature/patch (if applicable)] Short description of change. Links to relevant issues/PRs.`

- 1.1.8:
- Realtime text diff via invalidate diff on directory change
- Realtime text diff via invalidate diff on directory change [#867](https://github.com/FredrikNoren/ungit/pull/867)
- Promisify `./source/utils/cache.js` [#870](https://github.com/FredrikNoren/ungit/pull/870)
- 1.1.7:
- Fix diff flickering issue and optimization [#865](https://github.com/FredrikNoren/ungit/pull/865)
- Fix credential dialog issue [#864](https://github.com/FredrikNoren/ungit/pull/864)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"lodash": "~4.17.4",
"mkdirp": "~0.5.1",
"moment": "~2.17.1",
"node-cache": "~4.1.1",
"npm": "~4.1.1",
"npm-registry-client": "~7.4.5",
"octicons": "~3.5.0",
Expand Down
33 changes: 17 additions & 16 deletions source/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,27 +165,27 @@ if (config.authentication) {
};
}

const indexHtmlCache = cache((callback) => {
pluginsCache((plugins) => {
fs.readFile(__dirname + '/../public/index.html', (err, data) => {
async.map(Object.keys(plugins), (pluginName, callback) => {
plugins[pluginName].compile(callback);
}, (err, result) => {
const html = result.join('\n\n');
data = data.toString().replace('<!-- ungit-plugins-placeholder -->', html);
const indexHtmlCacheKey = cache.registerFunc(() => {
return cache.resolveFunc(pluginsCacheKey).then((plugins) => {
return fs.readFileAsync(__dirname + '/../public/index.html').then((data) => {
return Bluebird.all(Object.keys(plugins).map((pluginName) => {
return plugins[pluginName].compile();
})).then((results) => {
data = data.toString().replace('<!-- ungit-plugins-placeholder -->', results.join('\n\n'));
data = data.replace(/__ROOT_PATH__/g, config.rootPath);
callback(null, data);
});

return data;
})
});
});
});

app.get('/', (req, res) => {
if (config.dev) {
pluginsCache.invalidate();
indexHtmlCache.invalidate();
cache.invalidateFunc(pluginsCacheKey);
cache.invalidateFunc(indexHtmlCacheKey);
}
indexHtmlCache((err, data) => {
cache.resolveFunc(indexHtmlCacheKey).then((data) => {
res.end(data);
});
});
Expand Down Expand Up @@ -249,12 +249,13 @@ const loadPlugins = (plugins, pluginBasePath) => {
winston.info('Plugin loaded: ' + pluginDir);
});
}
const pluginsCache = cache((callback) => {
const pluginsCacheKey = cache.registerFunc(() => {
const plugins = [];
loadPlugins(plugins, path.join(__dirname, '..', 'components'));
if (fs.existsSync(config.pluginDirectory))
if (fs.existsSync(config.pluginDirectory)) {
loadPlugins(plugins, config.pluginDirectory);
callback(plugins);
}
return plugins;
});

app.get('/serverdata.js', (req, res) => {
Expand Down
82 changes: 40 additions & 42 deletions source/ungit-plugin.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

const fs = require('fs');
const path = require('path');
const async = require('async');
const express = require('express');
const winston = require('winston');
const config = require('./config');
const Bluebird = require('bluebird');

const assureArray = (obj) => { return Array.isArray(obj) ? obj : [obj]; }

Expand Down Expand Up @@ -38,54 +38,52 @@ class UngitPlugin {
env.app.use(`/plugins/${this.name}`, express.static(this.path));
}

compile(callback) {
compile() {
winston.info(`Compiling plugin ${this.path}`);
const exports = this.manifest.exports || {};
const tasks = [];

if (exports.raw) {
const raw = assureArray(exports.raw);
raw.forEach((rawSource) => {
tasks.push((callback) => {
fs.readFile(path.join(this.path, rawSource), (err, text) => {
callback(err, text + '\n');
return Bluebird.resolve().then(() => {
if (exports.raw) {
return Bluebird.all(assureArray(exports.raw).map((rawSource) => {
return fs.readFileAsync(path.join(this.path, rawSource)).then((text) => {
return text + '\n';
});
})).then((result) => {
return result.join('\n');
});
});
}

if (exports.javascript) {
const js = assureArray(exports.javascript);

js.forEach((filename) => {
tasks.push((callback) => {
callback(null, `<script type="text/javascript" src="${config.rootPath}/plugins/${this.name}/${filename}"></script>`);
});
});
}

if (exports.knockoutTemplates) {
Object.keys(exports.knockoutTemplates).forEach((templateName) => {
tasks.push((callback) => {
fs.readFile(path.join(this.path, exports.knockoutTemplates[templateName]), (err, text) => {
callback(err, `<script type="text/html" id="${templateName}">\n${text}\n</script>\n`);
} else {
return '';
}
}).then((result) => {
if (exports.javascript) {
return result + assureArray(exports.javascript).map(filename => {
return `<script type="text/javascript" src="${config.rootPath}/plugins/${this.name}/${filename}"></script>`;
}).join('\n');
} else {
return result;
}
}).then((result) => {
if (exports.knockoutTemplates) {
return Bluebird.all(Object.keys(exports.knockoutTemplates).map((templateName) => {
return fs.readFileAsync(path.join(this.path, exports.knockoutTemplates[templateName])).then((text) => {
return `<script type="text/html" id="${templateName}">\n${text}\n</script>`;
});
})).then((templates) => {
return result + templates.join('\n');
});
});
}

if (exports.css) {
const css = assureArray(exports.css);
css.forEach((cssSource) => {
tasks.push((callback) => {
callback(null, `<link rel="stylesheet" type="text/css" href="${config.rootPath}/plugins/${this.name}/${cssSource}" />`);
});
});
}

async.parallel(tasks, (err, result) => {
if (err) throw err;
callback(err, `<!-- Component: ${this.name} -->\n${result.join('\n')}`)
} else {
return result;
}
}).then((result) => {
if (exports.css) {
return result + assureArray(exports.css).map((cssSource) => {
return `<link rel="stylesheet" type="text/css" href="${config.rootPath}/plugins/${this.name}/${cssSource}" />`;
}).join('\n');
} else {
return result;
}
}).then((result) => {
return `<!-- Component: ${this.name} -->\n${result}`;
});
}
}
Expand Down
9 changes: 4 additions & 5 deletions source/usage-statistics.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ class UsageStatistics {
constructor() {
if (!config.sendUsageStatistics) return;
this.keen = keenio.configure({ projectId: _PROJECT_ID, writeKey: _WRITE_KEY });
this.getDefaultData = cache((callback) => {
sysinfo.getUserHash()
.then((hash) => {
callback(null, { version: config.ungitDevVersion, userHash: hash });
this.getDefaultDataKey = cache.registerFunc(() => {
return sysinfo.getUserHash().then((hash) => {
return { version: config.ungitDevVersion, userHash: hash };
});
});
}

_mergeDataWithDefaultData(data, callback) {
this.getDefaultData((err, defaultData) => {
cache.resolveFunc(this.getDefaultDataKey).then((defaultData) => {
data = data || {};
for(const k in defaultData)
data[k] = defaultData[k];
Expand Down
97 changes: 68 additions & 29 deletions source/utils/cache.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,75 @@
'use strict';
const Bluebird = require('bluebird');
const NodeCache = require('node-cache');
const cache = Bluebird.promisifyAll(new NodeCache({ stdTTL: 0, errorOnMissing: true }));
const md5 = require('blueimp-md5');
const funcMap = {}; // Will there ever be a use case where this is a cache with TTL? func registration with TTL?

const signals = require('signals');
/**
* @function resolveFunc
* @description Get cached result associated with the key or execute a function to get the result
* @param {string} [key] - A key associated with a function to be executed.
* @return {Promise} - Promise either resolved with cached result of the function or rejected with function not found.
*/
cache.resolveFunc = (key) => {
return cache.getAsync(key) // Cant do `cache.getAsync(key, true)` due to `get` argument ordering...
.catch({ errorcode: "ENOTFOUND" }, (e) => {
if (!funcMap[key]) throw e; // func associated with key is not found, throw not found error
const result = funcMap[key].func(); // func is found, resolve, set with TTL and return result
return cache.setAsync(key, result, funcMap[key].ttl)
.then(() => { return result });
});
}

// Wraps a function to produce one value at a time no matter how many times invoked, and cache that value until invalidated
module.exports = (constructValue) => {
let constructDone;
let hasCache = false;
/**
* @function registerFunc
* @description Register a function to cache it's result. If same key exists, key is deregistered and registered again.
* @param {ttl} [ttl=0] - ttl in seconds to be used for the cached result of function.
* @param {string} [key=md5 of func] - Key to retrive cached function result.
* @param {function} [func] - Function to be executed to get the result.
* @return {string} - key to retrive cached function result.
*/
cache.registerFunc = (...args) => {
let func = args.pop();
let key = args.pop() || md5(func);
let ttl = args.pop() || cache.options.stdTTL;

let f = (callback) => {
if (hasCache) return callback(f.error, f.value);
if (constructDone) return constructDone.add(callback);
if (typeof func !== "function") {
throw new Error("no function was passed in.");
}

constructDone = new signals.Signal();
let localConstructDone = constructDone;
constructDone.add((err, val) => {
constructDone = null;
callback(err, val);
});
constructValue((err, value) => {
hasCache = true;
f.error = err;
f.value = value;
localConstructDone.dispatch(err, value);
});
};
if (isNaN(ttl) || ttl < 0) {
throw new Error("ttl value is not valid.");
}

if (funcMap[key]) {
cache.deregisterFunc(key);
}

f.invalidate = () => {
hasCache = false;
f.error = null;
f.value = null;
constructDone = null;
};
funcMap[key] = {
func: func,
ttl: ttl
}

return f;
return key;
}

/**
* @function invalidateFunc
* @description Immediately invalidate cached function result despite ttl value
* @param {string} [key] - A key associated with a function to be executed.
*/
cache.invalidateFunc = (key) => {
cache.del(key);
}

/**
* @function deregisterFunc
* @description Remove function registration and invalidate it's cached value.
* @param {string} [key] - A key associated with a function to be executed.
*/
cache.deregisterFunc = (key) => {
cache.invalidateFunc(key);
delete funcMap[key];
}

module.exports = cache;
Loading

0 comments on commit 20d1c95

Please sign in to comment.