diff --git a/package.json b/package.json index 377a8bd..08d363f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "default-value": "0.0.3", "document-offset": "^1.0.4", "document-ready-promise": "^3.0.1", + "dotty": "0.0.2", "electron-prebuilt": "^1.0.1", "file-url": "^1.1.0", "in-array": "^0.1.2", diff --git a/src/app.js b/src/app.js index b0a85d7..9744e12 100644 --- a/src/app.js +++ b/src/app.js @@ -52,6 +52,10 @@ bar.on("ready", () => { //bar.showWindow(); }); +bar.on("request-quit", () => { + app.quit(); +}); + ipcMain.on("resize", (event, newSize) => { if (newSize.width != null) { bar.setOption("width", newSize.width); @@ -74,21 +78,5 @@ Promise.try(() => { }).then(() => { return rfr("lib/db/json-db")(path.join(appDirectories.userData(), "db.json")); }).then((db) => { - let metadataCache = db.collection("metadataCache"); - - const getPackageMetadata = rfr("lib/package/fetch-metadata")({ - CacheError: errors.CacheError, - get: function(packageName) { - try { - return metadataCache.findOne({name: packageName}); - } catch (err) { // FIXME - throw new errors.CacheError("Not in cache"); - } - }, - set: function(packageName, metadata) { - return metadataCache.upsertBy("name", metadata); - } - }); - }); \ No newline at end of file diff --git a/src/components/app/component.tag b/src/components/app/component.tag index 787c244..ac942af 100644 --- a/src/components/app/component.tag +++ b/src/components/app/component.tag @@ -2,57 +2,9 @@ app .logo-section img(src="../../assets/npm.svg", height=40) .bar-section - search-box - search-results(results="{results}") + search .window-height-marker - script. - const Promise = require("bluebird"); - const {ipcRenderer} = require("electron"); - const rfr = require("rfr"); - - const search = rfr("lib/search/constructor-io")("CD06z4gVeqSXRiDL2ZNK"); - - this.mixin(require("riot-query").mixin); - - let lastQuery; - this.results = [] - - this.on("mount", () => { - let searchBox = this.queryOne("search-box"); - let searchResults = this.queryOne("search-results"); - - searchBox.on("selectionUp", () => { - searchResults.moveSelectionUp(); - }); - - searchBox.on("selectionDown", () => { - searchResults.moveSelectionDown(); - }); - - searchBox.on("queryChanged", (query) => { - Promise.try(() => { - lastQuery = query; - return search(query); - }).then((results) => { - /* Sometimes, search results may come back out of order. Here we check whether - * the lastQuery is still the same as when we initially made the request. */ - if (lastQuery === query) { - this.results = results.packages; - this.update(); - } - }); - }); - - searchBox.on("cancel", () => { - ipcRenderer.send("closeSearch"); - }) - }); - - this.on("updated", () => { - global.triggerWindowResize(); - }); - style(scoped, type="scss"). .logo-section { position: absolute; diff --git a/src/components/app/index.js b/src/components/app/index.js index 76fc8a2..d386aa6 100644 --- a/src/components/app/index.js +++ b/src/components/app/index.js @@ -1,6 +1,5 @@ 'use strict'; -require("../search-box"); -require("../search-results"); +require("../search"); require("./component"); \ No newline at end of file diff --git a/src/components/search-results/component.tag b/src/components/search-results/component.tag index 930b37f..eb88def 100644 --- a/src/components/search-results/component.tag +++ b/src/components/search-results/component.tag @@ -1,7 +1,8 @@ search-results .result(each="{result, i in opts.results}", class="{active: i === currentSelection}") h2 {result.name} - .description {result.description} + .version v{result.latestVersion || "..."} + .description {result.description || "(no description)"} script. let lastResults; @@ -54,4 +55,10 @@ search-results .description { font-size: 13px; } + + .version { + font-style: italic; + float: right; + font-size: 13px; + } } \ No newline at end of file diff --git a/src/components/search/component.tag b/src/components/search/component.tag new file mode 100644 index 0000000..970d144 --- /dev/null +++ b/src/components/search/component.tag @@ -0,0 +1,83 @@ +search + search-box + search-results(results="{results}") + + script. + const Promise = require("bluebird"); + const path = require("path"); + const dotty = require("dotty"); + const {ipcRenderer} = require("electron"); + const rfr = require("rfr"); + + const appDirectory = require("appdirectory"); + let appDirectories = new appDirectory("npmbar"); + + const jsonDB = rfr("lib/db/json-db"); + const jsonDBCache = rfr("lib/db/json-db-cache"); + + this.mixin(require("riot-query").mixin); + + Object.assign(this, { + results: [] + }); + + let lastQuery; + + this.on("updated", () => { + global.triggerWindowResize(); + }); + + this.on("mount", () => { + Promise.try(() => { + return Promise.all([ + jsonDB(path.join(appDirectories.userData(), "search-cache.json")), + jsonDB(path.join(appDirectories.userData(), "package-cache.json")) + ]); + }).spread((searchCacheDB, packageCacheDB) => { + let searchCache = jsonDBCache(searchCacheDB, "searchCache"); + let packageCache = jsonDBCache(packageCacheDB, "packageCache"); + + const search = rfr("lib/search/constructor-io")("CD06z4gVeqSXRiDL2ZNK", searchCache); + const lookupPackage = rfr("lib/package/fetch-metadata")(packageCache); + + let searchBox = this.queryOne("search-box"); + let searchResults = this.queryOne("search-results"); + + searchBox.on("selectionUp", () => { + searchResults.moveSelectionUp(); + }); + + searchBox.on("selectionDown", () => { + searchResults.moveSelectionDown(); + }); + + searchBox.on("queryChanged", (query) => { + Promise.try(() => { + lastQuery = query; + return search(query); + }).then((results) => { + /* Sometimes, search results may come back out of order. Here we check whether + * the lastQuery is still the same as when we initially made the request. */ + if (lastQuery === query) { + this.results = results.packages; + this.update(); + + this.results.forEach((result, i) => { + Promise.try(() => { + return lookupPackage(result.name); + }).then((metadata) => { + if (dotty.exists(metadata, "dist-tags.latest")) { + result.latestVersion = metadata["dist-tags"].latest; + this.update(); + } + }) + }); + } + }); + }); + + searchBox.on("cancel", () => { + ipcRenderer.send("closeSearch"); + }) + }); + }); \ No newline at end of file diff --git a/src/components/search/index.js b/src/components/search/index.js new file mode 100644 index 0000000..76fc8a2 --- /dev/null +++ b/src/components/search/index.js @@ -0,0 +1,6 @@ +'use strict'; + +require("../search-box"); +require("../search-results"); + +require("./component"); \ No newline at end of file diff --git a/src/db/cacheize.js b/src/db/cacheize.js new file mode 100644 index 0000000..56b385a --- /dev/null +++ b/src/db/cacheize.js @@ -0,0 +1,38 @@ +'use strict'; + +const Promise = require("bluebird"); + +/* NOTE: This assumes that the first argument to the cacheized call, will be + * the cache key. */ + +module.exports = function(cache, cb) { + function getFromRemote(...args) { + return Promise.try(() => { + return cb(...args); + }).tap((result) => { + cache.set(args[0], Object.assign({ + $cacheKey: args[0] + }, result)); + }); + } + + function getFromCache(...args) { + return Promise.try(() => { + return cache.get(args[0]); + }).then((metadata) => { + if (metadata._cacheExpiry < Date.now()) { + throw new cache.CacheError("Cached data expired"); + } else { + return metadata; + } + }); + } + + return function(...args) { + return Promise.try(() => { + return getFromCache(...args); + }).catch(cache.CacheError, (err) => { + return getFromRemote(...args); + }); + } +} \ No newline at end of file diff --git a/src/db/json-db-cache.js b/src/db/json-db-cache.js new file mode 100644 index 0000000..60a1cdf --- /dev/null +++ b/src/db/json-db-cache.js @@ -0,0 +1,22 @@ +'use strict'; + +const rfr = require("rfr"); +const errors = rfr("lib/util/errors"); + +module.exports = function(db, collectionName) { + let cacheCollection = db.collection(collectionName); + + return { + CacheError: errors.CacheError, + get: function(packageName) { + try { + return cacheCollection.findOne({$cacheKey: packageName}); + } catch (err) { // FIXME + throw new errors.CacheError("Not in cache"); + } + }, + set: function(packageName, metadata) { + return cacheCollection.upsertBy("$cacheKey", metadata); + } + } +} \ No newline at end of file diff --git a/src/db/json-db.js b/src/db/json-db.js index 8bb314c..04e872b 100644 --- a/src/db/json-db.js +++ b/src/db/json-db.js @@ -25,21 +25,28 @@ const rfr = require("rfr"); const promiseDebounce = rfr("lib/util/promise-debounce"); const escapeCollectionName = rfr("lib/db/escape-collection-name"); // FIXME: Actually split this up into collections... +/* We don't use locks in development mode, as various build tools won't play nice with this. */ +let developmentMode = (process.env.NODE_ENV === "development"); + function getLockPath(path) { return `${path}.lock`; } function obtainLock(path) { return Promise.try(() => { - return fs.writeFileAsync(getLockPath(path), "", { - flag: "wx" - }); + if (!developmentMode) { + return fs.writeFileAsync(getLockPath(path), "", { + flag: "wx" + }); + } }); } function releaseLock(path) { return Promise.try(() => { - return fs.unlinkAsync(getLockPath(path)); + if (!developmentMode) { + return fs.unlinkAsync(getLockPath(path)); + } }); } @@ -160,12 +167,15 @@ module.exports = function(path) { let db = { opened: true, + closing: false, close: function() { return Promise.try(() => { + this.closing = true; return queueWrite(); }).then(() => { return releaseLock(path); }).then(() => { + this.closing = false; this.opened = false; }); }, @@ -181,20 +191,48 @@ module.exports = function(path) { function cleanup() { console.log("Doing cleanup..."); if (db.opened === true) { - db.close(); + return db.close(); } } - if (process.versions.electron != null) { - /* Running in Electron */ - require("electron").app.on("will-quit", () => { - cleanup(); - }) - } else { - /* Running in a different Node-y environment */ - process.on("beforeExit", () => { - cleanup(); - }); + if (!developmentMode) { + if (process.versions.electron != null) { + /* Running in Electron */ + let {app, ipcRenderer} = require("electron"); + + if (app != null) { + /* Main process */ + app.on("will-quit", () => { + cleanup(); + }); + } else if (ipcRenderer != null) { + /* Renderer process */ + window.addEventListener("beforeunload", (event) => { + if (db.opened === true) { + if (db.closing === false) { + Promise.try(() => { + return cleanup(); + }).then(() => { + ipcRenderer.send("request-quit"); + }); + } + + return false; + } + }); + ipcRenderer.on("will-quit", () => { + cleanup(); + }) + } else { + // ??? + } + + } else { + /* Running in a different Node-y environment */ + process.on("beforeExit", () => { + cleanup(); + }); + } } return db; diff --git a/src/package/fetch-metadata.js b/src/package/fetch-metadata.js index 686a445..5344ef8 100644 --- a/src/package/fetch-metadata.js +++ b/src/package/fetch-metadata.js @@ -2,9 +2,12 @@ const Promise = require("bluebird"); const bhttp = require("bhttp"); +const rfr = require("rfr"); + +const cacheize = rfr("lib/db/cacheize"); module.exports = function(cache) { - function getFromRemote(packageName) { + return cacheize(cache, function(packageName) { return Promise.try(() => { let encodedPackageName = encodeURIComponent(packageName).replace(/%40/g, "@"); return bhttp.get(`https://registry.npmjs.org/${encodedPackageName}`); @@ -15,28 +18,6 @@ module.exports = function(cache) { } else { return response.body; } - }).tap((metadata) => { - cache.set(packageName, metadata); - }); - } - - function getFromCache(packageName) { - return Promise.try(() => { - return cache.get(packageName); - }).then((metadata) => { - if (metadata._cacheExpiry < Date.now()) { - throw new cache.CacheError("Cached data expired"); - } else { - return metadata; - } - }); - } - - return function(packageName) { - return Promise.try(() => { - return getFromCache(packageName); - }).catch(cache.CacheError, (err) => { - return getFromRemote(packageName); }); - } + }); } \ No newline at end of file diff --git a/src/search/constructor-io.js b/src/search/constructor-io.js index c68f410..9e40665 100644 --- a/src/search/constructor-io.js +++ b/src/search/constructor-io.js @@ -4,8 +4,11 @@ const Promise = require("bluebird"); const bhttp = require("bhttp"); const querystring = require("querystring"); const defaultValue = require("default-value"); +const rfr = require("rfr"); -module.exports = function(autocompleteKey) { +const cacheize = rfr("lib/db/cacheize"); + +module.exports = function(autocompleteKey, cache) { function createQueryString(query, options) { return querystring.stringify({ autocomplete_key: autocompleteKey, @@ -19,14 +22,13 @@ module.exports = function(autocompleteKey) { return `https://ac.cnstrc.com/autocomplete/${encodeURIComponent(query)}?${createQueryString(query, options)}`; } - return function doSearch(query, options = {}) { + return cacheize(cache, function doSearch(query, options = {}) { return Promise.try(() => { if (query.trim() !== "") { return Promise.try(() => { return bhttp.get(createUrl(query, options)); }).then((response) => { if (response.statusCode === 200) { - console.log(response.body); return { packages: response.body.sections.packages.map((pkg) => { return { @@ -47,6 +49,5 @@ module.exports = function(autocompleteKey) { } } }) - - } + }) } \ No newline at end of file