Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds cache to autocompletion mite-api requests #83 #104

Merged
merged 13 commits into from
Feb 28, 2021
Merged
2 changes: 2 additions & 0 deletions source/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ try {
}

nconf.defaults({
cacheFilename: path.resolve(path.join(homedir, '.mite-cli-cache.json')),
cacheTtl: 5 * 24 * 3600, // default ttl for file caches set to 7 days
currency: '€',
applicationName: `mite-cli/${pkg.version}`,
customersColumns: customersCommand.columns.default,
Expand Down
79 changes: 79 additions & 0 deletions source/lib/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';
const fs = require('fs');

class Cache {

constructor(filename) {
this.filename = filename;
this.loaded = false;
this.cache = {};
}

serialize(cache) {
return JSON.stringify(cache);
}

deserialize(string) {
return JSON.parse(string);
}

createCacheItem(value, options) {
const now = new Date();
const timestamp = now.getTime();
const expire = options.expire || timestamp + (options.ttl * 1000);
return { value, timestamp, expire };
}

isExpired(item) {
if (!item) return true;
return item.expire <= new Date().getTime();
}

async load() {
if (this.loaded) {
return this;
}
const content = await fs.promises.readFile(this.filename);
this.cache = this.deserialize(content);
this.loaded = true;
return this;
}

async save() {
await fs.promises.writeFile(this.filename, this.serialize(this.cache));
return this;
}

key(input) {
return JSON.stringify(input);
}

clear() {
this.cache = {};
return this;
}

async set(key, value, { ttl, expire }) {
await this.load();
this.cache[this.key(key)] = this.createCacheItem(value, { ttl, expire });
return this;
}

async get(key) {
await this.load();
const item = this.cache[this.key(key)];
// only return the item’s value when it’s not expired
if (item && !this.isExpired(item)) {
return item.value;
}
return undefined;
}

async delete(key) {
await this.load();
delete this.cache[this.key(key)];
return this;
}
}

module.exports = Cache;
55 changes: 33 additions & 22 deletions source/lib/mite-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ const util = require('util');
const assert = require('assert');

const miteApi = require('mite-api');

const MiteTracker = require('./mite-tracker');
const { BUDGET_TYPE } = require('./../lib/constants');
const Cache = require('./cache');

/**
* @typedef MiteTimeEntryTracker
Expand Down Expand Up @@ -58,6 +58,7 @@ class MiteApiWrapper {
this.config = config;
this.mite = miteApi(config);
this.tracker = new MiteTracker(config);
this.cache = new Cache(config.cacheFilename);
}

/**
Expand Down Expand Up @@ -292,14 +293,19 @@ class MiteApiWrapper {
return a - b;
}

filterItem(item, regexp) {
return regexp.test((item || {}).name);
removeItemByArchived(item, archivedFlag) {
if (typeof archivedFlag === 'boolean') {
return item.archived === archivedFlag;
}
return true;
}

filterItems(items, query) {
if (!query) return items;
const queryRegexp = new RegExp(query, 'i');
return items.filter(item => this.filterItem(item, queryRegexp));
itemMatchQuery(item, query) {
if (query) {
const regexp = new RegExp(query, 'i');
return regexp.test((item || {}).name);
}
return true;
}

/**
Expand All @@ -318,21 +324,26 @@ class MiteApiWrapper {
};
const itemNamePluralCamelCased = itemName.substr(0, 1).toUpperCase() + itemName.substr(1) + 's';
const opts = Object.assign({}, defaultOpts, options);
return Promise.all([
util.promisify(this.mite['get' + itemNamePluralCamelCased])(opts),
util.promisify(this.mite['getArchived' + itemNamePluralCamelCased])(opts),
])
.then(results => Array.prototype.concat.apply([], results))
.then(items => items.map(c => c[itemName]))
.then(items => items.filter(item => {
if (typeof options.archived === 'boolean') {
return item.archived === options.archived;
}
return true;
}))
.then(items => this.filterItems(items, options.query))
// always sort by name
.then(items => this.sort(items, 'name'));

const cacheKey = ['getItemsAndArchived', itemName, options];
let items = await this.cache.get(cacheKey);
if (!items) {
items = await Promise.all([
util.promisify(this.mite['get' + itemNamePluralCamelCased])(opts),
util.promisify(this.mite['getArchived' + itemNamePluralCamelCased])(opts),
]);
items = Array.prototype.concat.apply([], items)
.map(c => c[itemName])
.filter(item => this.removeItemByArchived(item, options.archived))
.filter(item => this.itemMatchQuery(item, options.query));
this.sort(items, 'name');

// cache values for 24 hours
await this.cache.set(cacheKey, items, { ttl: this.config.cacheTtl });
await this.cache.save();
}

return items;
}

async getCustomers (options = {}) {
Expand Down