Skip to content

Commit 04d866c

Browse files
authored
feat: adds cache to autocompletion mite-api requests #83 (#104)
* feat: adds cache to miteapi requests #83 * style: lint fix * feat: adds cache to miteapi requests #83 * Chore: fix codeclimate issue * chore: removes unused flat-cache
1 parent 23ffe23 commit 04d866c

File tree

3 files changed

+114
-22
lines changed

3 files changed

+114
-22
lines changed

source/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ try {
2424
}
2525

2626
nconf.defaults({
27+
cacheFilename: path.resolve(path.join(homedir, '.mite-cli-cache.json')),
28+
cacheTtl: 5 * 24 * 3600, // default ttl for file caches set to 7 days
2729
currency: '€',
2830
applicationName: `mite-cli/${pkg.version}`,
2931
customersColumns: customersCommand.columns.default,

source/lib/cache.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use strict';
2+
const fs = require('fs');
3+
4+
class Cache {
5+
6+
constructor(filename) {
7+
this.filename = filename;
8+
this.loaded = false;
9+
this.cache = {};
10+
}
11+
12+
serialize(cache) {
13+
return JSON.stringify(cache);
14+
}
15+
16+
deserialize(string) {
17+
return JSON.parse(string);
18+
}
19+
20+
createCacheItem(value, options) {
21+
const now = new Date();
22+
const timestamp = now.getTime();
23+
const expire = options.expire || timestamp + (options.ttl * 1000);
24+
return { value, timestamp, expire };
25+
}
26+
27+
isExpired(item) {
28+
if (!item) return true;
29+
return item.expire <= new Date().getTime();
30+
}
31+
32+
async load() {
33+
if (this.loaded) {
34+
return this;
35+
}
36+
const content = await fs.promises.readFile(this.filename);
37+
this.cache = this.deserialize(content);
38+
this.loaded = true;
39+
return this;
40+
}
41+
42+
async save() {
43+
await fs.promises.writeFile(this.filename, this.serialize(this.cache));
44+
return this;
45+
}
46+
47+
key(input) {
48+
return JSON.stringify(input);
49+
}
50+
51+
clear() {
52+
this.cache = {};
53+
return this;
54+
}
55+
56+
async set(key, value, { ttl, expire }) {
57+
await this.load();
58+
this.cache[this.key(key)] = this.createCacheItem(value, { ttl, expire });
59+
return this;
60+
}
61+
62+
async get(key) {
63+
await this.load();
64+
const item = this.cache[this.key(key)];
65+
// only return the item’s value when it’s not expired
66+
if (item && !this.isExpired(item)) {
67+
return item.value;
68+
}
69+
return undefined;
70+
}
71+
72+
async delete(key) {
73+
await this.load();
74+
delete this.cache[this.key(key)];
75+
return this;
76+
}
77+
}
78+
79+
module.exports = Cache;

source/lib/mite-api.js

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ const util = require('util');
44
const assert = require('assert');
55

66
const miteApi = require('mite-api');
7-
87
const MiteTracker = require('./mite-tracker');
98
const { BUDGET_TYPE } = require('./../lib/constants');
9+
const Cache = require('./cache');
1010

1111
/**
1212
* @typedef MiteTimeEntryTracker
@@ -58,6 +58,7 @@ class MiteApiWrapper {
5858
this.config = config;
5959
this.mite = miteApi(config);
6060
this.tracker = new MiteTracker(config);
61+
this.cache = new Cache(config.cacheFilename);
6162
}
6263

6364
/**
@@ -292,14 +293,19 @@ class MiteApiWrapper {
292293
return a - b;
293294
}
294295

295-
filterItem(item, regexp) {
296-
return regexp.test((item || {}).name);
296+
removeItemByArchived(item, archivedFlag) {
297+
if (typeof archivedFlag === 'boolean') {
298+
return item.archived === archivedFlag;
299+
}
300+
return true;
297301
}
298302

299-
filterItems(items, query) {
300-
if (!query) return items;
301-
const queryRegexp = new RegExp(query, 'i');
302-
return items.filter(item => this.filterItem(item, queryRegexp));
303+
itemMatchQuery(item, query) {
304+
if (query) {
305+
const regexp = new RegExp(query, 'i');
306+
return regexp.test((item || {}).name);
307+
}
308+
return true;
303309
}
304310

305311
/**
@@ -318,21 +324,26 @@ class MiteApiWrapper {
318324
};
319325
const itemNamePluralCamelCased = itemName.substr(0, 1).toUpperCase() + itemName.substr(1) + 's';
320326
const opts = Object.assign({}, defaultOpts, options);
321-
return Promise.all([
322-
util.promisify(this.mite['get' + itemNamePluralCamelCased])(opts),
323-
util.promisify(this.mite['getArchived' + itemNamePluralCamelCased])(opts),
324-
])
325-
.then(results => Array.prototype.concat.apply([], results))
326-
.then(items => items.map(c => c[itemName]))
327-
.then(items => items.filter(item => {
328-
if (typeof options.archived === 'boolean') {
329-
return item.archived === options.archived;
330-
}
331-
return true;
332-
}))
333-
.then(items => this.filterItems(items, options.query))
334-
// always sort by name
335-
.then(items => this.sort(items, 'name'));
327+
328+
const cacheKey = ['getItemsAndArchived', itemName, options];
329+
let items = await this.cache.get(cacheKey);
330+
if (!items) {
331+
items = await Promise.all([
332+
util.promisify(this.mite['get' + itemNamePluralCamelCased])(opts),
333+
util.promisify(this.mite['getArchived' + itemNamePluralCamelCased])(opts),
334+
]);
335+
items = Array.prototype.concat.apply([], items)
336+
.map(c => c[itemName])
337+
.filter(item => this.removeItemByArchived(item, options.archived))
338+
.filter(item => this.itemMatchQuery(item, options.query));
339+
this.sort(items, 'name');
340+
341+
// cache values for 24 hours
342+
await this.cache.set(cacheKey, items, { ttl: this.config.cacheTtl });
343+
await this.cache.save();
344+
}
345+
346+
return items;
336347
}
337348

338349
async getCustomers (options = {}) {

0 commit comments

Comments
 (0)