Skip to content

Commit

Permalink
MemoryCache and SingleOperation
Browse files Browse the repository at this point in the history
  • Loading branch information
joelfillmore committed Mar 18, 2022
1 parent 068b217 commit 7028d39
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto

# Explicitly declare text files we want to always be normalized and converted
# to native line endings on checkout.
*.js text
*.less text
*.html text

# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.gif binary
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
MemoryCache: require('./src/MemoryCache'),
SingleOperation: require('./src/SingleOperation'),
};
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@thinkpixellab-public/px-long-operations",
"version": "1.0.0",
"description": "Utilities for long running operations",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/thinkpixellab/px-long-operations.git"
},
"author": "Pixel Lab",
"license": "MIT",
"bugs": {
"url": "https://github.com/thinkpixellab/px-long-operations/issues"
},
"homepage": "https://github.com/thinkpixellab/px-long-operations#readme"
}
106 changes: 106 additions & 0 deletions src/MemoryCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
var SingleOperation = require('./SingleOperation');

class MemoryCache {
constructor({ defaultTTL = 5 * 60 * 1000, ttlByKey = {}, debug = false } = {}) {
this.cache = {};
this.singleOperation = new SingleOperation();

// default time to live in ms
this.ttlByKey = ttlByKey;
this.defaultTTL = defaultTTL;

// enable logging in debug mode
this.debug = !!debug;
//console.log('MemoryCache options: ' + JSON.stringify(options));
}

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

async ensure(key, valueFactory, { ttl, valueErrorRetries = 3, allowLazyRefresh = false } = {}) {
var refreshValue = oldValue => {
return this.singleOperation.run(key, async () => {
let retry = 0;
while (true) {
try {
const newValue = await valueFactory(oldValue);
this.set(key, newValue, ttl);
return newValue;
} catch (error) {
// automatically retry up to limit
retry++;
if (retry > valueErrorRetries) {
return Promise.reject(error);
}
}
}
});
};

var entry = key ? this.cache[key] : null;
if (!entry) {
return refreshValue();
}

if (Date.now() < entry.expires) {
// not yet expired
return entry.value;
} else if (allowLazyRefresh) {
// refresh in background, return expired result
refreshValue(entry.value).catch(error => {
// log but suppress any errors because they happen on a background thread
console.error('Error refreshing cache value: ' + key);
console.error(error);
});
return entry.value;
} else {
// refresh and wait
return refreshValue(entry.value);
}
}

ensureLazy(key, valueFactory, { ttl, valueErrorRetries = 3 } = {}) {
return this.ensure(key, valueFactory, { ttl, valueErrorRetries, allowLazyRefresh: true });
}

get(key, allowExpired) {
if (key) {
var entry = this.cache[key];
if (entry) {
if (allowExpired || Date.now() < entry.expires) {
return entry.value;
} else if (this.debug) {
console.log('expired cache entry: ' + key);
}
}
}

if (this.debug) {
console.log('no cache entry: ' + key);
}
return undefined;
}

set(key, value, ttl) {
// fallback to config then default ttl if not specified
ttl = ttl || this.ttlByKey[key] || this.defaultTTL;

var expires = Date.now() + ttl;
this.cache[key] = new CacheEntry(value, expires);

if (this.debug) {
const ttlSeconds = Math.round(ttl / 1000);
console.log('caching ' + key + ' (ttl:' + ttlSeconds + ' secs)');
}
}
}

class CacheEntry {
constructor(value, expires) {
this.value = value;
this.expires = expires;
}
}

module.exports = MemoryCache;
19 changes: 19 additions & 0 deletions src/SingleOperation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class SingleOperation {
constructor() {
this.pending = {};
}

run(key, valueFactory) {
var operationPromise = this.pending[key];
if (operationPromise) {
return operationPromise;
}

var promise = (this.pending[key] = valueFactory().finally(() => {
delete this.pending[key];
}));

return promise;
}
}
module.exports = SingleOperation;

0 comments on commit 7028d39

Please sign in to comment.